Moved to CUE validation

This commit is contained in:
Martino Ferrari
2026-01-23 11:16:06 +01:00
parent 5c3f05a1a4
commit 5853365707
15 changed files with 511 additions and 477 deletions

View File

@@ -2,8 +2,12 @@ package validator
import (
"fmt"
"strconv"
"strings"
"cuelang.org/go/cue"
"cuelang.org/go/cue/errors"
"github.com/marte-dev/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/schema"
@@ -68,37 +72,9 @@ func (v *Validator) validateNode(node *index.ProjectNode) {
}
}
// Collect fields and their definitions
fields := v.getFields(node)
fieldOrder := []string{}
for _, frag := range node.Fragments {
for _, def := range frag.Definitions {
if f, ok := def.(*parser.Field); ok {
if _, exists := fields[f.Name]; exists { // already collected
// Maintain order logic if needed, but getFields collects all.
// For strict order check we might need this loop.
// Let's assume getFields is enough for validation logic,
// but for "duplicate check" and "class validation" we iterate fields map.
// We need to construct fieldOrder.
// Just reuse loop for fieldOrder
}
}
}
}
// Re-construct fieldOrder for order validation
seen := make(map[string]bool)
for _, frag := range node.Fragments {
for _, def := range frag.Definitions {
if f, ok := def.(*parser.Field); ok {
if !seen[f.Name] {
fieldOrder = append(fieldOrder, f.Name)
seen[f.Name] = true
}
}
}
}
// 1. Check for duplicate fields
// 1. Check for duplicate fields (Go logic)
for name, defs := range fields {
if len(defs) > 1 {
firstFile := v.getFileForField(defs[0], node)
@@ -139,11 +115,9 @@ func (v *Validator) validateNode(node *index.ProjectNode) {
}
}
// 3. Schema Validation
// 3. CUE Validation
if className != "" && v.Schema != nil {
if classDef, ok := v.Schema.Classes[className]; ok {
v.validateClass(node, classDef, fields, fieldOrder)
}
v.validateWithCUE(node, className)
}
// 4. Signal Validation (for DataSource signals)
@@ -162,68 +136,95 @@ func (v *Validator) validateNode(node *index.ProjectNode) {
}
}
func (v *Validator) validateClass(node *index.ProjectNode, classDef schema.ClassDefinition, fields map[string][]*parser.Field, fieldOrder []string) {
// ... (same as before)
for _, fieldDef := range classDef.Fields {
if fieldDef.Mandatory {
found := false
if _, ok := fields[fieldDef.Name]; ok {
found = true
} else if fieldDef.Type == "node" {
if _, ok := node.Children[fieldDef.Name]; ok {
found = true
}
}
func (v *Validator) validateWithCUE(node *index.ProjectNode, className string) {
// Check if class exists in schema
classPath := cue.ParsePath(fmt.Sprintf("#Classes.%s", className))
if v.Schema.Value.LookupPath(classPath).Err() != nil {
return // Unknown class, skip validation
}
if !found {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: fmt.Sprintf("Missing mandatory field '%s' for class '%s'", fieldDef.Name, node.Metadata["Class"]),
Position: v.getNodePosition(node),
File: v.getNodeFile(node),
})
}
// Convert node to map
data := v.nodeToMap(node)
// Encode data to CUE
dataVal := v.Schema.Context.Encode(data)
// Unify with #Object
// #Object requires "Class" field, which is present in data.
objDef := v.Schema.Value.LookupPath(cue.ParsePath("#Object"))
// Unify
res := objDef.Unify(dataVal)
if err := res.Validate(cue.Concrete(true)); err != nil {
// Report errors
// Parse CUE error to diagnostic
v.reportCUEError(err, node)
}
}
func (v *Validator) reportCUEError(err error, node *index.ProjectNode) {
list := errors.Errors(err)
for _, e := range list {
msg := e.Error()
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: fmt.Sprintf("Schema Validation Error: %v", msg),
Position: v.getNodePosition(node),
File: v.getNodeFile(node),
})
}
}
func (v *Validator) nodeToMap(node *index.ProjectNode) map[string]interface{} {
m := make(map[string]interface{})
fields := v.getFields(node)
for name, defs := range fields {
if len(defs) > 0 {
// Use the last definition (duplicates checked elsewhere)
m[name] = v.valueToInterface(defs[len(defs)-1].Value)
}
}
for _, fieldDef := range classDef.Fields {
if fList, ok := fields[fieldDef.Name]; ok {
f := fList[0]
if !v.checkType(f.Value, fieldDef.Type) {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: fmt.Sprintf("Field '%s' expects type '%s'", fieldDef.Name, fieldDef.Type),
Position: f.Position,
File: v.getFileForField(f, node),
})
}
}
// Children as nested maps?
// CUE schema expects nested structs for "node" type fields.
// But `node.Children` contains ALL children (even those defined as +Child).
// If schema expects `States: { ... }`, we map children.
for name, child := range node.Children {
// normalize name? CUE keys are strings.
// If child real name is "+States", key in Children is "States".
// We use "States" as key in map.
m[name] = v.nodeToMap(child)
}
if classDef.Ordered {
schemaIdx := 0
for _, nodeFieldName := range fieldOrder {
foundInSchema := false
for i, fd := range classDef.Fields {
if fd.Name == nodeFieldName {
foundInSchema = true
if i < schemaIdx {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: fmt.Sprintf("Field '%s' is out of order", nodeFieldName),
Position: fields[nodeFieldName][0].Position,
File: v.getFileForField(fields[nodeFieldName][0], node),
})
} else {
schemaIdx = i
}
break
}
}
if !foundInSchema {
}
return m
}
func (v *Validator) valueToInterface(val parser.Value) interface{} {
switch t := val.(type) {
case *parser.StringValue:
return t.Value
case *parser.IntValue:
i, _ := strconv.ParseInt(t.Raw, 0, 64)
return i // CUE handles int64
case *parser.FloatValue:
f, _ := strconv.ParseFloat(t.Raw, 64)
return f
case *parser.BoolValue:
return t.Value
case *parser.ReferenceValue:
return t.Value
case *parser.ArrayValue:
var arr []interface{}
for _, e := range t.Elements {
arr = append(arr, v.valueToInterface(e))
}
return arr
}
return nil
}
func (v *Validator) validateSignal(node *index.ProjectNode, fields map[string][]*parser.Field) {
@@ -308,12 +309,17 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di
}
}
// Check Direction
// Check Direction using CUE Schema
dsClass := v.getNodeClass(dsNode)
if dsClass != "" {
if classDef, ok := v.Schema.Classes[dsClass]; ok {
dsDir := classDef.Direction
if dsDir != "" {
// Lookup class definition in Schema
// path: #Classes.ClassName.direction
path := cue.ParsePath(fmt.Sprintf("#Classes.%s.direction", dsClass))
val := v.Schema.Value.LookupPath(path)
if val.Err() == nil {
dsDir, err := val.String()
if err == nil && dsDir != "" {
if direction == "Input" && dsDir == "OUT" {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
@@ -537,32 +543,7 @@ func isValidType(t string) bool {
}
func (v *Validator) checkType(val parser.Value, expectedType string) bool {
// ... (same as before)
switch expectedType {
case "int":
_, ok := val.(*parser.IntValue)
return ok
case "float":
_, ok := val.(*parser.FloatValue)
return ok
case "string":
_, okStr := val.(*parser.StringValue)
_, okRef := val.(*parser.ReferenceValue)
return okStr || okRef
case "bool":
_, ok := val.(*parser.BoolValue)
return ok
case "array":
_, ok := val.(*parser.ArrayValue)
return ok
case "reference":
_, ok := val.(*parser.ReferenceValue)
return ok
case "node":
return true
case "any":
return true
}
// Legacy function, replaced by CUE.
return true
}
@@ -679,7 +660,8 @@ func isDataSource(node *index.ProjectNode) bool {
if node.Parent != nil && node.Parent.Name == "Data" {
return true
}
return false
_, hasSignals := node.Children["Signals"]
return hasSignals
}
func isSignal(node *index.ProjectNode) bool {