Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion pkg/cmd/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@
// The CLI is based on cobra and viper.
// * https://github.com/spf13/cobra
// * https://github.com/spf13/viper
//
package cmd
24 changes: 12 additions & 12 deletions pkg/gen/filters/common/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package common

import (
"fmt"
"slices"
"reflect"
"slices"

"github.com/ettle/strcase"
"github.com/gertd/go-pluralize"
Expand Down Expand Up @@ -49,31 +49,31 @@ 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)
}
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)
}
2 changes: 0 additions & 2 deletions pkg/gen/filters/filterqt/qt_namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

6 changes: 3 additions & 3 deletions pkg/gen/filters/filterqt/qt_namespace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
func TestQtNamespace(t *testing.T) {
t.Parallel()
var tests = []struct {
in string
in string
result string
}{
{"", ""},
Expand All @@ -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)
})
}
}
}
2 changes: 1 addition & 1 deletion pkg/gen/filters/filterqt/qt_param.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
6 changes: 3 additions & 3 deletions pkg/sim/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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")
Expand Down
8 changes: 4 additions & 4 deletions pkg/sim/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
6 changes: 3 additions & 3 deletions pkg/sim/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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))
Expand Down
1 change: 0 additions & 1 deletion pkg/sim/utils_test.go
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
package sim

61 changes: 47 additions & 14 deletions pkg/spec/schema/apigear.module.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down Expand Up @@ -85,18 +85,18 @@
},
"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.",
"type": "string"
},
"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": {
Expand Down Expand Up @@ -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"
}
Expand All @@ -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": {
Expand All @@ -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"
}
},
Expand All @@ -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.",
Expand All @@ -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": {
Expand Down Expand Up @@ -259,15 +292,15 @@
"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": {
"description": "Meta information are read by the individual template. Read the template manual which meta tags are supported.",
"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"
}
},
Expand All @@ -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.",
Expand Down
Loading
Loading