diff --git a/pkg/cmd/doc.go b/pkg/cmd/doc.go index d88e36fd..7c9d4c1d 100644 --- a/pkg/cmd/doc.go +++ b/pkg/cmd/doc.go @@ -2,5 +2,4 @@ // The CLI is based on cobra and viper. // * https://github.com/spf13/cobra // * https://github.com/spf13/viper -// package cmd diff --git a/pkg/gen/filters/common/helper.go b/pkg/gen/filters/common/helper.go index d34229bd..1b3d44c4 100644 --- a/pkg/gen/filters/common/helper.go +++ b/pkg/gen/filters/common/helper.go @@ -2,8 +2,8 @@ package common import ( "fmt" - "slices" "reflect" + "slices" "github.com/ettle/strcase" "github.com/gertd/go-pluralize" @@ -49,22 +49,22 @@ func Pluralize(s string, i int) string { } func unpackArray(s any) []any { - v := reflect.ValueOf(s) - r := make([]any, v.Len()) - for i := 0; i < v.Len(); i++ { - r[i] = v.Index(i).Interface() - } - return r + v := reflect.ValueOf(s) + r := make([]any, v.Len()) + for i := 0; i < v.Len(); i++ { + r[i] = v.Index(i).Interface() + } + return r } func CollectFields(items any, fieldName string) ([]string, error) { - list := []string {} + list := []string{} unpacked := unpackArray(items) for _, item := range unpacked { - r := reflect.ValueOf(item) + r := reflect.ValueOf(item) reflectValue := reflect.Indirect(r).FieldByName(fieldName) if !reflectValue.IsValid() { - return list, fmt.Errorf("given struct %T has no field %s ", item, fieldName) + return list, fmt.Errorf("given struct %T has no field %s ", item, fieldName) } value := reflectValue.String() list = append(list, value) @@ -72,8 +72,8 @@ func CollectFields(items any, fieldName string) ([]string, error) { return list, nil } -func Unique (inList []string) []string { +func Unique(inList []string) []string { list := slices.Clone(inList) slices.Sort(list) - return slices.Compact(list) + return slices.Compact(list) } diff --git a/pkg/gen/filters/filterqt/qt_namespace.go b/pkg/gen/filters/filterqt/qt_namespace.go index b5120250..64acbfff 100644 --- a/pkg/gen/filters/filterqt/qt_namespace.go +++ b/pkg/gen/filters/filterqt/qt_namespace.go @@ -4,10 +4,8 @@ import ( "github.com/apigear-io/cli/pkg/gen/filters/common" ) - // qtNamespace returns the input string with style applied for creating a namespace name func qtNamespace(name string) string { return common.SnakeCaseLower(name) } - diff --git a/pkg/gen/filters/filterqt/qt_namespace_test.go b/pkg/gen/filters/filterqt/qt_namespace_test.go index 572e25bd..d7f1049d 100644 --- a/pkg/gen/filters/filterqt/qt_namespace_test.go +++ b/pkg/gen/filters/filterqt/qt_namespace_test.go @@ -9,7 +9,7 @@ import ( func TestQtNamespace(t *testing.T) { t.Parallel() var tests = []struct { - in string + in string result string }{ {"", ""}, @@ -19,11 +19,11 @@ func TestQtNamespace(t *testing.T) { {"NAMESPACE", "namespace"}, {"Name Space", "name_space"}, } - + for _, testLine := range tests { t.Run(testLine.in, func(t *testing.T) { out := qtNamespace(testLine.in) assert.Equal(t, testLine.result, out) }) } -} \ No newline at end of file +} diff --git a/pkg/gen/filters/filterqt/qt_param.go b/pkg/gen/filters/filterqt/qt_param.go index 877682ba..81326f39 100644 --- a/pkg/gen/filters/filterqt/qt_param.go +++ b/pkg/gen/filters/filterqt/qt_param.go @@ -36,7 +36,7 @@ func ToParamString(prefix string, schema *model.Schema, name string) (string, er ex := schema.LookupExtern(schema.Import, schema.Type) if ex != nil { exQt := qtExtern(schema.GetExtern()) - namespace :="" + namespace := "" if exQt.NameSpace != "" { namespace = fmt.Sprintf("%s::", exQt.NameSpace) } diff --git a/pkg/sim/api.go b/pkg/sim/api.go index d7b11daf..613aaa05 100644 --- a/pkg/sim/api.go +++ b/pkg/sim/api.go @@ -33,12 +33,12 @@ func (w *World) CreateService(object string, properties map[string]any) (any, er w.servicesLoaded = true service := NewObjectService(w.engine, object, properties) w.services[object] = service - + // If called from JavaScript, return a proxy if w.engine.rt != nil { return CreateServiceProxy(w.engine.rt, service), nil } - + // If called from Go (e.g., tests), return the service directly return service, nil } @@ -53,7 +53,7 @@ func (w *World) GetService(object string) *ObjectService { func (w *World) register(rt *goja.Runtime) { // Keep the engine runtime reference for proxy creation w.engine.rt = rt - + // Register $createService directly (no need for proxy.js anymore) if err := rt.Set("$createService", w.CreateService); err != nil { log.Error().Err(err).Msg("failed to set $createService") diff --git a/pkg/sim/engine.go b/pkg/sim/engine.go index 26e58530..2f1ca59c 100644 --- a/pkg/sim/engine.go +++ b/pkg/sim/engine.go @@ -84,19 +84,19 @@ func NewEngine(opts EngineOptions) *Engine { } e.world = NewWorld(e) e.loop.Start() - + // Initial setup - wait for initialization to complete before returning // This ensures e.rt is set and the engine is fully ready done := make(chan bool) e.loop.RunOnLoop(func(rt *goja.Runtime) { - e.rt = rt // Set the runtime once during initialization + e.rt = rt // Set the runtime once during initialization rt.SetFieldNameMapper(goja.UncapFieldNameMapper()) e.world.register(rt) registry.Enable(rt) done <- true }) - <-done // Wait for initialization to complete - + <-done // Wait for initialization to complete + return e } diff --git a/pkg/sim/service.go b/pkg/sim/service.go index f62d73d9..f7f742fe 100644 --- a/pkg/sim/service.go +++ b/pkg/sim/service.go @@ -35,10 +35,10 @@ func NewObjectService(engine *Engine, objectId string, properties map[string]any } s.source = NewOLinkSource(s) s.engine.registerSource(s.source) - + // Create the proxy for this service s.proxy = CreateServiceProxy(engine.rt, s) - + return s } @@ -154,7 +154,7 @@ func (o *ObjectService) RemoveProperty(name string) { func (o *ObjectService) EmitSignal(signal string, args ...any) { // Emit locally to JavaScript listeners o.signalEmitter.Emit(signal, args) - + // Also notify OLink clients if source is available if o.source != nil { o.source.NotifySignal(signal, core.Args(args)) diff --git a/pkg/sim/utils_test.go b/pkg/sim/utils_test.go index 95ecbaae..b34659a8 100644 --- a/pkg/sim/utils_test.go +++ b/pkg/sim/utils_test.go @@ -1,2 +1 @@ package sim - diff --git a/pkg/spec/schema/apigear.module.schema.json b/pkg/spec/schema/apigear.module.schema.json index 1a0b5bb7..7c0a60de 100644 --- a/pkg/spec/schema/apigear.module.schema.json +++ b/pkg/spec/schema/apigear.module.schema.json @@ -50,7 +50,7 @@ "type": "string" }, "value": { - "description": "The value of the enum member. It must be unique within the enum and must be a positive integer. If no value is specified, the first member will be assigned the value 0, and subsequent members will be assigned incrementing values.", + "description": "The non-negative integer value of the enum member, unique within the enum. Auto-numbering is all-or-nothing: if you omit 'value' on EVERY member, members are numbered 0, 1, 2 ... by position; but as soon as you set 'value' on ANY member, auto-numbering is disabled and every member left without a 'value' stays 0 (which may collide). So specify either all values or none.", "minimum": 0, "type": "integer" } @@ -85,7 +85,7 @@ }, "Interface": { "additionalProperties": false, - "description": "An interface is a collection of reactive properties, callable operations and emittable signals. Names of properties, operations and signals must be unique within the interface.", + "description": "An interface is a collection of reactive properties, callable operations and emittable signals. Names of properties, operations and signals share one namespace and must all be unique within the interface (this includes each property's auto-generated change signal).", "properties": { "description": { "description": "A description of the interface. This should provide a clear and concise explanation of the interface's purpose and functionality.", @@ -93,10 +93,10 @@ }, "extends": { "additionalProperties": false, - "description": "An interface can extend another interface, inheriting its properties and operations. If the interface comes from another module, the import field must be set.", + "description": "References a single base interface. Inheritance is technology-neutral and delegated to the target template, which is expected to render it as native inheritance (e.g. C++ ': public Base', Java 'extends Base'). Declare only the members this interface ADDS - do NOT re-declare the base's properties, operations or signals, as that would produce redundant overrides or shadowing in the generated code. Set 'import' when the base interface is in another module. Note: support is template-dependent and the bundled templates do not currently render extends, so inherited members may be absent in generated output.", "properties": { "import": { - "description": "The name of the module where the base interface is defined.", + "description": "The name of the module where the base interface is defined. If it is another module, that module must also be listed in the top-level 'imports' array.", "type": "string" }, "name": { @@ -127,16 +127,16 @@ "type": "array" }, "properties": { - "description": "An array of reactive properties. Templates automatically generate setter, getter and change signal for each property (set\u003cName\u003e, get\u003cName\u003e, on\u003cName\u003e).", + "description": "An array of reactive properties. For each property the generated SDK automatically provides a getter, a setter (unless 'readonly' is set) and a change-notification signal that fires whenever the value changes. Do NOT declare a separate signal to report a property change - it already exists. Exact generated names (e.g. getX/setX/onXChanged) depend on the target language template.", "items": { "$ref": "#/definitions/NamedType" }, "type": "array" }, "signals": { - "description": "An array of emittablesignals, with parameters.", + "description": "An array of signals the interface can emit. Declare only signals that are NOT already implied by a property change (see 'properties').", "items": { - "$ref": "#/definitions/Operation" + "$ref": "#/definitions/Signal" }, "type": "array" } @@ -159,7 +159,7 @@ "type": "string" }, "import": { - "description": "If the type is defined in another module, this is the name of that module.", + "description": "If the type is defined in another module, this is that module's name. The module must also be listed in the top-level 'imports' array, otherwise the reference will not resolve.", "type": "string" }, "meta": { @@ -172,11 +172,11 @@ "type": "string" }, "readonly": { - "description": "If true, the type is readonly and no setter will be generated, meaning it cannot be modified after creation.", + "description": "Only meaningful for interface properties: if true, the property is read-only and no setter is generated. Has no effect on struct fields or operation parameters.", "type": "boolean" }, "type": { - "description": "A type either a primitive type (int, int16, int32, int64, float, float16, float32, float64, bool, string) or a declared type name (e.g. an interface name, struct name or enum name).", + "description": "The type: either a primitive type (bool, int, int32, int64, float, float32, float64, string, bytes, any) or the name of a declared type (an interface, struct or enum). Note: there is no int16/float16. For a type declared in another module, also set 'import' (see below).", "type": "string" } }, @@ -188,7 +188,7 @@ }, "Operation": { "additionalProperties": false, - "description": "An operation is a callable interface method, with parameters and a return type.", + "description": "An operation is a callable interface method with parameters and an optional return type. Omit 'return' (or set its type to 'void') for operations that return nothing.", "properties": { "description": { "description": "A description of the operation. Should be a short, descriptive text about the operation.", @@ -214,6 +214,39 @@ "$ref": "#/definitions/Type" } }, + "required": [ + "name" + ], + "type": "object" + }, + "Signal": { + "additionalProperties": false, + "description": "A signal is a one-way notification emitted by the interface to its subscribers. Signals carry parameters but never return a value.", + "properties": { + "description": { + "description": "A description of the signal. Should be a short, descriptive text about when the signal is emitted.", + "type": "string" + }, + "meta": { + "description": "Meta information is evaluated by the individual template. Read the template manual which meta tags are supported.", + "type": "object" + }, + "name": { + "description": "A name starts with a letter, followed by zero-or-more letters, numbers or underscores.", + "pattern": "^[a-zA-Z][0-9A-Za-z_]*$", + "type": "string" + }, + "params": { + "description": "An array of signal parameters.", + "items": { + "$ref": "#/definitions/NamedType" + }, + "type": "array" + } + }, + "required": [ + "name" + ], "type": "object" }, "Struct": { @@ -259,7 +292,7 @@ "type": "string" }, "import": { - "description": "If the type is defined in another module, this is the name of that module.", + "description": "If the type is defined in another module, this is that module's name. The module must also be listed in the top-level 'imports' array, otherwise the reference will not resolve.", "type": "string" }, "meta": { @@ -267,7 +300,7 @@ "type": "object" }, "type": { - "description": "A primitive type or a defined type (such as interface, struct or enum).", + "description": "The return type: a primitive type (bool, int, int32, int64, float, float32, float64, string, bytes, any) or a declared type (interface, struct or enum).", "type": "string" } }, @@ -277,7 +310,7 @@ "type": "object" } }, - "description": "This is the schema for the ObjectAPI 1.0 specification.\nIt is used to validate the ObjectAPI 1.0 specification.\n\nAn example of a valid ObjectAPI 1.0 specification is:\n\n```\nschema: apigear.module/1.0\nname: my.module\nversion: 1.0\ninterfaces:\n - name: Counter\n properties:\n - name: value\n type: int\n operations:\n - name: increment\n params:\n - name: value\n type: int\n return: int\n - name: decrement\n params:\n - name: value\n type: int\n return: int\n```\n", + "description": "This is the schema for the ObjectAPI 1.0 specification.\nIt is used to validate the ObjectAPI 1.0 specification.\n\nModel overview: a module groups interfaces, structs and enums. An interface\ndescribes a reactive object that may live locally or be accessed remotely\n(e.g. over ApiGear ObjectLink). It has three member kinds:\n - properties: observable state. Each property automatically gets a getter,\n a setter (unless readonly) and a change-notification signal - never add a\n separate signal just to report a property change.\n - operations: callable methods with parameters and an optional return value;\n depending on the target they may complete asynchronously.\n - signals: one-way notifications emitted to subscribers; they carry\n parameters but never return a value.\nProperty, operation and signal names share one namespace per interface and\nmust all be unique (including each property's implicit change signal). Types\nfrom another module require both an 'import' on the type and an entry in the\ntop-level 'imports' array.\n\nAn example of a valid ObjectAPI 1.0 specification is:\n\n```\nschema: apigear.module/1.0\nname: my.module\nversion: 1.0\ninterfaces:\n - name: Counter\n properties:\n - name: value\n type: int\n operations:\n - name: increment\n params:\n - name: value\n type: int\n return: int\n - name: decrement\n params:\n - name: value\n type: int\n return: int\n```\n", "properties": { "description": { "description": "Module description. Should be a short description of the module.", diff --git a/pkg/spec/schema/apigear.module.schema.yaml b/pkg/spec/schema/apigear.module.schema.yaml index ca6d726a..6e1679be 100644 --- a/pkg/spec/schema/apigear.module.schema.yaml +++ b/pkg/spec/schema/apigear.module.schema.yaml @@ -3,6 +3,21 @@ description: | This is the schema for the ObjectAPI 1.0 specification. It is used to validate the ObjectAPI 1.0 specification. + Model overview: a module groups interfaces, structs and enums. An interface + describes a reactive object that may live locally or be accessed remotely + (e.g. over ApiGear ObjectLink). It has three member kinds: + - properties: observable state. Each property automatically gets a getter, + a setter (unless readonly) and a change-notification signal - never add a + separate signal just to report a property change. + - operations: callable methods with parameters and an optional return value; + depending on the target they may complete asynchronously. + - signals: one-way notifications emitted to subscribers; they carry + parameters but never return a value. + Property, operation and signal names share one namespace per interface and + must all be unique (including each property's implicit change signal). Types + from another module require both an 'import' on the type and an entry in the + top-level 'imports' array. + An example of a valid ObjectAPI 1.0 specification is: ``` @@ -110,16 +125,16 @@ definitions: description: "Meta information are read by the individual template. Read the template manual which meta tags are supported." type: type: string - description: "A type either a primitive type (int, int16, int32, int64, float, float16, float32, float64, bool, string) or a declared type name (e.g. an interface name, struct name or enum name)." + description: "The type: either a primitive type (bool, int, int32, int64, float, float32, float64, string, bytes, any) or the name of a declared type (an interface, struct or enum). Note: there is no int16/float16. For a type declared in another module, also set 'import' (see below)." import: type: string - description: "If the type is defined in another module, this is the name of that module." + description: "If the type is defined in another module, this is that module's name. The module must also be listed in the top-level 'imports' array, otherwise the reference will not resolve." array: type: boolean description: "If true, the type is an array of the given type." readonly: type: boolean - description: "If true, the type is readonly and no setter will be generated, meaning it cannot be modified after creation." + description: "Only meaningful for interface properties: if true, the property is read-only and no setter is generated. Has no effect on struct fields or operation parameters." Type: type: object description: "A type information without a name. (e.g. int), used for return types." @@ -134,10 +149,10 @@ definitions: description: "Meta information are read by the individual template. Read the template manual which meta tags are supported." type: type: string - description: "A primitive type or a defined type (such as interface, struct or enum)." + description: "The return type: a primitive type (bool, int, int32, int64, float, float32, float64, string, bytes, any) or a declared type (interface, struct or enum)." import: type: string - description: "If the type is defined in another module, this is the name of that module." + description: "If the type is defined in another module, this is that module's name. The module must also be listed in the top-level 'imports' array, otherwise the reference will not resolve." array: type: boolean description: "If true, the type is an array of the given type." @@ -159,7 +174,7 @@ definitions: description: "Meta information is evaluated by the individual template. Read the template manual which meta tags are supported." Interface: type: object - description: "An interface is a collection of reactive properties, callable operations and emittable signals. Names of properties, operations and signals must be unique within the interface." + description: "An interface is a collection of reactive properties, callable operations and emittable signals. Names of properties, operations and signals share one namespace and must all be unique within the interface (this includes each property's auto-generated change signal)." required: [name] additionalProperties: false properties: @@ -172,7 +187,7 @@ definitions: description: "A description of the interface. This should provide a clear and concise explanation of the interface's purpose and functionality." extends: type: object - description: "An interface can extend another interface, inheriting its properties and operations. If the interface comes from another module, the import field must be set." + description: "References a single base interface. Inheritance is technology-neutral and delegated to the target template, which is expected to render it as native inheritance (e.g. C++ ': public Base', Java 'extends Base'). Declare only the members this interface ADDS - do NOT re-declare the base's properties, operations or signals, as that would produce redundant overrides or shadowing in the generated code. Set 'import' when the base interface is in another module. Note: support is template-dependent and the bundled templates do not currently render extends, so inherited members may be absent in generated output." required: [name] additionalProperties: false properties: @@ -182,13 +197,13 @@ definitions: pattern: "^[a-zA-Z][0-9A-Za-z_]*$" import: type: string - description: "The name of the module where the base interface is defined." + description: "The name of the module where the base interface is defined. If it is another module, that module must also be listed in the top-level 'imports' array." meta: type: object description: "Meta information is evaluated by the individual template. Read the template manual which meta tags are supported." properties: type: array - description: "An array of reactive properties. Templates automatically generate setter, getter and change signal for each property (set, get, on)." + description: "An array of reactive properties. For each property the generated SDK automatically provides a getter, a setter (unless 'readonly' is set) and a change-notification signal that fires whenever the value changes. Do NOT declare a separate signal to report a property change - it already exists. Exact generated names (e.g. getX/setX/onXChanged) depend on the target language template." items: $ref: "#/definitions/NamedType" operations: @@ -198,9 +213,9 @@ definitions: $ref: "#/definitions/Operation" signals: type: array - description: "An array of emittablesignals, with parameters." + description: "An array of signals the interface can emit. Declare only signals that are NOT already implied by a property change (see 'properties')." items: - $ref: "#/definitions/Operation" + $ref: "#/definitions/Signal" Struct: type: object description: "A struct is a message format that groups related data fields under a single name." @@ -245,8 +260,9 @@ definitions: $ref: "#/definitions/EnumMember" Operation: type: object - description: "An operation is a callable interface method, with parameters and a return type." + description: "An operation is a callable interface method with parameters and an optional return type. Omit 'return' (or set its type to 'void') for operations that return nothing." additionalProperties: false + required: [name] properties: name: type: string @@ -265,6 +281,27 @@ definitions: description: "An array of operation parameters." items: $ref: "#/definitions/NamedType" + Signal: + type: object + description: "A signal is a one-way notification emitted by the interface to its subscribers. Signals carry parameters but never return a value." + additionalProperties: false + required: [name] + properties: + name: + type: string + description: "A name starts with a letter, followed by zero-or-more letters, numbers or underscores." + pattern: "^[a-zA-Z][0-9A-Za-z_]*$" + description: + type: string + description: "A description of the signal. Should be a short, descriptive text about when the signal is emitted." + meta: + type: object + description: "Meta information is evaluated by the individual template. Read the template manual which meta tags are supported." + params: + type: array + description: "An array of signal parameters." + items: + $ref: "#/definitions/NamedType" EnumMember: # member value inside an enum type: object description: "An enum member is a name, value pair." @@ -283,5 +320,5 @@ definitions: description: "Meta information are read by the individual template. Read the template manual which meta tags are supported." value: type: integer - description: "The value of the enum member. It must be unique within the enum and must be a positive integer. If no value is specified, the first member will be assigned the value 0, and subsequent members will be assigned incrementing values." + description: "The non-negative integer value of the enum member, unique within the enum. Auto-numbering is all-or-nothing: if you omit 'value' on EVERY member, members are numbered 0, 1, 2 ... by position; but as soon as you set 'value' on ANY member, auto-numbering is disabled and every member left without a 'value' stays 0 (which may collide). So specify either all values or none." minimum: 0