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

277
internal/schema/marte.cue Normal file
View File

@@ -0,0 +1,277 @@
package schema
#Classes: {
RealTimeApplication: {
Functions: {...} // type: node
Data!: {...} // type: node
States!: {...} // type: node
...
}
StateMachine: {
...
}
RealTimeState: {
Threads: {...} // type: node
...
}
RealTimeThread: {
Functions: [...] // type: array
...
}
GAMScheduler: {
TimingDataSource: string // type: reference
...
}
TimingDataSource: {
direction: "IN"
...
}
IOGAM: {
InputSignals?: {...} // type: node
OutputSignals?: {...} // type: node
...
}
ReferenceContainer: {
...
}
ConstantGAM: {
...
}
PIDGAM: {
Kp: float | int // type: float (allow int as it promotes)
Ki: float | int
Kd: float | int
...
}
FileDataSource: {
Filename: string
Format?: string
direction: "INOUT"
...
}
LoggerDataSource: {
direction: "OUT"
...
}
DANStream: {
Timeout?: int
direction: "OUT"
...
}
EPICSCAInput: {
direction: "IN"
...
}
EPICSCAOutput: {
direction: "OUT"
...
}
EPICSPVAInput: {
direction: "IN"
...
}
EPICSPVAOutput: {
direction: "OUT"
...
}
SDNSubscriber: {
Address: string
Port: int
Interface?: string
direction: "IN"
...
}
SDNPublisher: {
Address: string
Port: int
Interface?: string
direction: "OUT"
...
}
UDPReceiver: {
Port: int
Address?: string
direction: "IN"
...
}
UDPSender: {
Destination: string
direction: "OUT"
...
}
FileReader: {
Filename: string
Format?: string
Interpolate?: string
direction: "IN"
...
}
FileWriter: {
Filename: string
Format?: string
StoreOnTrigger?: int
direction: "OUT"
...
}
OrderedClass: {
First: int
Second: string
...
}
BaseLib2GAM: {...}
ConversionGAM: {...}
DoubleHandshakeGAM: {...}
FilterGAM: {
Num: [...]
Den: [...]
ResetInEachState?: _
InputSignals?: {...}
OutputSignals?: {...}
...
}
HistogramGAM: {
BeginCycleNumber?: int
StateChangeResetName?: string
InputSignals?: {...}
OutputSignals?: {...}
...
}
Interleaved2FlatGAM: {...}
FlattenedStructIOGAM: {...}
MathExpressionGAM: {
Expression: string
InputSignals?: {...}
OutputSignals?: {...}
...
}
MessageGAM: {...}
MuxGAM: {...}
SimulinkWrapperGAM: {...}
SSMGAM: {...}
StatisticsGAM: {...}
TimeCorrectionGAM: {...}
TriggeredIOGAM: {...}
WaveformGAM: {...}
DAN: {
direction: "OUT"
...
}
LinuxTimer: {
ExecutionMode?: string
SleepNature?: string
SleepPercentage?: _
Phase?: int
CPUMask?: int
TimeProvider?: {...}
Signals: {...}
direction: "IN"
...
}
LinkDataSource: {
direction: "INOUT"
...
}
MDSReader: {
TreeName: string
ShotNumber: int
Frequency: float | int
Signals: {...}
direction: "IN"
...
}
MDSWriter: {
NumberOfBuffers: int
CPUMask: int
StackSize: int
TreeName: string
PulseNumber?: int
StoreOnTrigger: int
EventName: string
TimeRefresh: float | int
NumberOfPreTriggers?: int
NumberOfPostTriggers?: int
Signals: {...}
Messages?: {...}
direction: "OUT"
...
}
NI1588TimeStamp: {
direction: "IN"
...
}
NI6259ADC: {
direction: "IN"
...
}
NI6259DAC: {
direction: "OUT"
...
}
NI6259DIO: {
direction: "INOUT"
...
}
NI6368ADC: {
direction: "IN"
...
}
NI6368DAC: {
direction: "OUT"
...
}
NI6368DIO: {
direction: "INOUT"
...
}
NI9157CircularFifoReader: {
direction: "IN"
...
}
NI9157MxiDataSource: {
direction: "INOUT"
...
}
OPCUADSInput: {
direction: "IN"
...
}
OPCUADSOutput: {
direction: "OUT"
...
}
RealTimeThreadAsyncBridge: {...}
RealTimeThreadSynchronisation: {...}
UARTDataSource: {
direction: "INOUT"
...
}
BaseLib2Wrapper: {...}
EPICSCAClient: {...}
EPICSPVA: {...}
MemoryGate: {...}
OPCUA: {...}
SysLogger: {...}
GAMDataSource: {
direction: "INOUT"
...
}
}
// Definition for any Object.
// It must have a Class field.
// Based on Class, it validates against #Classes.
#Object: {
Class: string
// Allow any other field by default (extensibility),
// unless #Classes definition is closed.
// We allow open structs now.
...
// Unify if Class is known.
// If Class is NOT in #Classes, this might fail or do nothing depending on CUE logic.
// Actually, `#Classes[Class]` fails if key is missing.
// This ensures we validate against known classes.
// If we want to allow unknown classes, we need a check.
// But spec implies validation should check known classes.
#Classes[Class]
}

View File

@@ -1,237 +0,0 @@
{
"classes": {
"RealTimeApplication": {
"fields": [
{"name": "Functions", "type": "node", "mandatory": true},
{"name": "Data", "type": "node", "mandatory": true},
{"name": "States", "type": "node", "mandatory": true}
]
},
"StateMachine": {
"fields": [
{"name": "States", "type": "node", "mandatory": false}
]
},
"RealTimeState": {
"fields": [
{"name": "Threads", "type": "node", "mandatory": true}
]
},
"RealTimeThread": {
"fields": [
{"name": "Functions", "type": "array", "mandatory": true}
]
},
"GAMScheduler": {
"fields": [
{"name": "TimingDataSource", "type": "reference", "mandatory": true}
]
},
"TimingDataSource": {
"fields": [],
"direction": "IN"
},
"IOGAM": {
"fields": [
{"name": "InputSignals", "type": "node", "mandatory": false},
{"name": "OutputSignals", "type": "node", "mandatory": false}
]
},
"ReferenceContainer": {
"fields": []
},
"ConstantGAM": {
"fields": []
},
"PIDGAM": {
"fields": [
{"name": "Kp", "type": "float", "mandatory": true},
{"name": "Ki", "type": "float", "mandatory": true},
{"name": "Kd", "type": "float", "mandatory": true}
]
},
"FileDataSource": {
"fields": [
{"name": "Filename", "type": "string", "mandatory": true},
{"name": "Format", "type": "string", "mandatory": false}
],
"direction": "INOUT"
},
"LoggerDataSource": {
"fields": [],
"direction": "OUT"
},
"DANStream": {
"fields": [
{"name": "Timeout", "type": "int", "mandatory": false}
],
"direction": "OUT"
},
"EPICSCAInput": {
"fields": [],
"direction": "IN"
},
"EPICSCAOutput": {
"fields": [],
"direction": "OUT"
},
"EPICSPVAInput": {
"fields": [],
"direction": "IN"
},
"EPICSPVAOutput": {
"fields": [],
"direction": "OUT"
},
"SDNSubscriber": {
"fields": [
{"name": "Address", "type": "string", "mandatory": true},
{"name": "Port", "type": "int", "mandatory": true},
{"name": "Interface", "type": "string", "mandatory": false}
],
"direction": "IN"
},
"SDNPublisher": {
"fields": [
{"name": "Address", "type": "string", "mandatory": true},
{"name": "Port", "type": "int", "mandatory": true},
{"name": "Interface", "type": "string", "mandatory": false}
],
"direction": "OUT"
},
"UDPReceiver": {
"fields": [
{"name": "Port", "type": "int", "mandatory": true},
{"name": "Address", "type": "string", "mandatory": false}
],
"direction": "IN"
},
"UDPSender": {
"fields": [
{"name": "Destination", "type": "string", "mandatory": true}
],
"direction": "OUT"
},
"FileReader": {
"fields": [
{"name": "Filename", "type": "string", "mandatory": true},
{"name": "Format", "type": "string", "mandatory": false},
{"name": "Interpolate", "type": "string", "mandatory": false}
],
"direction": "IN"
},
"FileWriter": {
"fields": [
{"name": "Filename", "type": "string", "mandatory": true},
{"name": "Format", "type": "string", "mandatory": false},
{"name": "StoreOnTrigger", "type": "int", "mandatory": false}
],
"direction": "OUT"
},
"OrderedClass": {
"ordered": true,
"fields": [
{"name": "First", "type": "int", "mandatory": true},
{"name": "Second", "type": "string", "mandatory": true}
]
},
"BaseLib2GAM": { "fields": [] },
"ConversionGAM": { "fields": [] },
"DoubleHandshakeGAM": { "fields": [] },
"FilterGAM": {
"fields": [
{"name": "Num", "type": "array", "mandatory": true},
{"name": "Den", "type": "array", "mandatory": true},
{"name": "ResetInEachState", "type": "any", "mandatory": false},
{"name": "InputSignals", "type": "node", "mandatory": false},
{"name": "OutputSignals", "type": "node", "mandatory": false}
]
},
"HistogramGAM": {
"fields": [
{"name": "BeginCycleNumber", "type": "int", "mandatory": false},
{"name": "StateChangeResetName", "type": "string", "mandatory": false},
{"name": "InputSignals", "type": "node", "mandatory": false},
{"name": "OutputSignals", "type": "node", "mandatory": false}
]
},
"Interleaved2FlatGAM": { "fields": [] },
"FlattenedStructIOGAM": { "fields": [] },
"MathExpressionGAM": {
"fields": [
{"name": "Expression", "type": "string", "mandatory": true},
{"name": "InputSignals", "type": "node", "mandatory": false},
{"name": "OutputSignals", "type": "node", "mandatory": false}
]
},
"MessageGAM": { "fields": [] },
"MuxGAM": { "fields": [] },
"SimulinkWrapperGAM": { "fields": [] },
"SSMGAM": { "fields": [] },
"StatisticsGAM": { "fields": [] },
"TimeCorrectionGAM": { "fields": [] },
"TriggeredIOGAM": { "fields": [] },
"WaveformGAM": { "fields": [] },
"DAN": { "fields": [], "direction": "OUT" },
"LinuxTimer": {
"fields": [
{"name": "ExecutionMode", "type": "string", "mandatory": false},
{"name": "SleepNature", "type": "string", "mandatory": false},
{"name": "SleepPercentage", "type": "any", "mandatory": false},
{"name": "Phase", "type": "int", "mandatory": false},
{"name": "CPUMask", "type": "int", "mandatory": false},
{"name": "TimeProvider", "type": "node", "mandatory": false},
{"name": "Signals", "type": "node", "mandatory": true}
],
"direction": "IN"
},
"LinkDataSource": { "fields": [], "direction": "INOUT" },
"MDSReader": {
"fields": [
{"name": "TreeName", "type": "string", "mandatory": true},
{"name": "ShotNumber", "type": "int", "mandatory": true},
{"name": "Frequency", "type": "float", "mandatory": true},
{"name": "Signals", "type": "node", "mandatory": true}
],
"direction": "IN"
},
"MDSWriter": {
"fields": [
{"name": "NumberOfBuffers", "type": "int", "mandatory": true},
{"name": "CPUMask", "type": "int", "mandatory": true},
{"name": "StackSize", "type": "int", "mandatory": true},
{"name": "TreeName", "type": "string", "mandatory": true},
{"name": "PulseNumber", "type": "int", "mandatory": false},
{"name": "StoreOnTrigger", "type": "int", "mandatory": true},
{"name": "EventName", "type": "string", "mandatory": true},
{"name": "TimeRefresh", "type": "float", "mandatory": true},
{"name": "NumberOfPreTriggers", "type": "int", "mandatory": false},
{"name": "NumberOfPostTriggers", "type": "int", "mandatory": false},
{"name": "Signals", "type": "node", "mandatory": true},
{"name": "Messages", "type": "node", "mandatory": false}
],
"direction": "OUT"
},
"NI1588TimeStamp": { "fields": [], "direction": "IN" },
"NI6259ADC": { "fields": [], "direction": "IN" },
"NI6259DAC": { "fields": [], "direction": "OUT" },
"NI6259DIO": { "fields": [], "direction": "INOUT" },
"NI6368ADC": { "fields": [], "direction": "IN" },
"NI6368DAC": { "fields": [], "direction": "OUT" },
"NI6368DIO": { "fields": [], "direction": "INOUT" },
"NI9157CircularFifoReader": { "fields": [], "direction": "IN" },
"NI9157MxiDataSource": { "fields": [], "direction": "INOUT" },
"OPCUADSInput": { "fields": [], "direction": "IN" },
"OPCUADSOutput": { "fields": [], "direction": "OUT" },
"RealTimeThreadAsyncBridge": { "fields": [] },
"RealTimeThreadSynchronisation": { "fields": [] },
"UARTDataSource": { "fields": [], "direction": "INOUT" },
"BaseLib2Wrapper": { "fields": [] },
"EPICSCAClient": { "fields": [] },
"EPICSPVA": { "fields": [] },
"MemoryGate": { "fields": [] },
"OPCUA": { "fields": [] },
"SysLogger": { "fields": [] },
"GAMDataSource": { "fields": [], "direction": "INOUT" }
}
}

View File

@@ -2,137 +2,73 @@ package schema
import (
_ "embed"
"encoding/json"
"fmt"
"os"
"path/filepath"
"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
)
//go:embed marte.json
var defaultSchemaJSON []byte
//go:embed marte.cue
var defaultSchemaCUE []byte
type Schema struct {
Classes map[string]ClassDefinition `json:"classes"`
}
type ClassDefinition struct {
Fields []FieldDefinition `json:"fields"`
Ordered bool `json:"ordered"`
Direction string `json:"direction"`
}
type FieldDefinition struct {
Name string `json:"name"`
Type string `json:"type"` // "int", "float", "string", "bool", "reference", "array", "node", "any"
Mandatory bool `json:"mandatory"`
Context *cue.Context
Value cue.Value
}
func NewSchema() *Schema {
ctx := cuecontext.New()
return &Schema{
Classes: make(map[string]ClassDefinition),
Context: ctx,
Value: ctx.CompileBytes(defaultSchemaCUE),
}
}
func LoadSchema(path string) (*Schema, error) {
// LoadSchema loads a CUE schema from a file and returns the cue.Value
func LoadSchema(ctx *cue.Context, path string) (cue.Value, error) {
content, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var s Schema
if err := json.Unmarshal(content, &s); err != nil {
return nil, fmt.Errorf("failed to parse schema: %v", err)
}
return &s, nil
}
// DefaultSchema returns the built-in embedded schema
func DefaultSchema() *Schema {
var s Schema
if err := json.Unmarshal(defaultSchemaJSON, &s); err != nil {
panic(fmt.Sprintf("failed to parse default embedded schema: %v", err))
}
if s.Classes == nil {
s.Classes = make(map[string]ClassDefinition)
}
return &s
}
// Merge adds rules from 'other' to 's'.
// Rules for the same class are merged (new fields added, existing fields updated).
func (s *Schema) Merge(other *Schema) {
if other == nil {
return
}
for className, classDef := range other.Classes {
if existingClass, ok := s.Classes[className]; ok {
// Merge fields
fieldMap := make(map[string]FieldDefinition)
for _, f := range classDef.Fields {
fieldMap[f.Name] = f
}
var mergedFields []FieldDefinition
seen := make(map[string]bool)
// Keep existing fields, update if present in other
for _, f := range existingClass.Fields {
if newF, ok := fieldMap[f.Name]; ok {
mergedFields = append(mergedFields, newF)
} else {
mergedFields = append(mergedFields, f)
}
seen[f.Name] = true
}
// Append new fields
for _, f := range classDef.Fields {
if !seen[f.Name] {
mergedFields = append(mergedFields, f)
}
}
existingClass.Fields = mergedFields
if classDef.Ordered {
existingClass.Ordered = true
}
if classDef.Direction != "" {
existingClass.Direction = classDef.Direction
}
s.Classes[className] = existingClass
} else {
s.Classes[className] = classDef
}
return cue.Value{}, err
}
return ctx.CompileBytes(content), nil
}
func LoadFullSchema(projectRoot string) *Schema {
s := DefaultSchema()
ctx := cuecontext.New()
baseVal := ctx.CompileBytes(defaultSchemaCUE)
if baseVal.Err() != nil {
// Fallback or panic? Panic is appropriate for embedded schema failure
panic(fmt.Sprintf("Embedded schema invalid: %v", baseVal.Err()))
}
// 1. System Paths
sysPaths := []string{
"/usr/share/mdt/marte_schema.json",
"/usr/share/mdt/marte_schema.cue",
}
home, err := os.UserHomeDir()
if err == nil {
sysPaths = append(sysPaths, filepath.Join(home, ".local/share/mdt/marte_schema.json"))
sysPaths = append(sysPaths, filepath.Join(home, ".local/share/mdt/marte_schema.cue"))
}
for _, path := range sysPaths {
if sysSchema, err := LoadSchema(path); err == nil {
s.Merge(sysSchema)
if val, err := LoadSchema(ctx, path); err == nil && val.Err() == nil {
baseVal = baseVal.Unify(val)
}
}
// 2. Project Path
if projectRoot != "" {
projectSchemaPath := filepath.Join(projectRoot, ".marte_schema.json")
if projSchema, err := LoadSchema(projectSchemaPath); err == nil {
s.Merge(projSchema)
projectSchemaPath := filepath.Join(projectRoot, ".marte_schema.cue")
if val, err := LoadSchema(ctx, projectSchemaPath); err == nil && val.Err() == nil {
baseVal = baseVal.Unify(val)
}
}
return s
return &Schema{
Context: ctx,
Value: baseVal,
}
}