diff --git a/codegen/README.md b/codegen/README.md index eddc5b1..6a5a9af 100644 --- a/codegen/README.md +++ b/codegen/README.md @@ -31,5 +31,14 @@ in CI keeps it in sync with the schema. See `xbot_service_interface_go/codegen` implementation and the golden tests. Not supported yet: outputs (DATA received from the service) need a receive loop in the Go client, -and arrays other than `char[n]`/`uint8_t[n]` are rejected while generating (loudly) instead of -producing broken code. +and unknown array element types are rejected while generating (loudly) instead of producing broken +code. + +Identifiers are translated to the Go naming rules for initialisms (`SoundId` -> `SoundID`, +`GpsQuality` -> `GPSQuality`; see `xbot_service_interface_go/codegen/names.go`). The original name +from the `service.json` stays visible, so the schema remains the reference a reader greps for: + +- doc comments on types, RPC methods, register setters and input senders + (`// PlayMP3 calls the RPC function 3 ("PlayMp3" in the service.json).`), +- trailing comments on the enum values (`SoundIDBootPing SoundID = 0 // BOOT_PING`), +- nothing on the `Fn*` function-id constants: they are only used together with the RPC method whose doc comment names the schema function. diff --git a/xbot_service_interface_go/README.md b/xbot_service_interface_go/README.md index a56298c..dec55c6 100644 --- a/xbot_service_interface_go/README.md +++ b/xbot_service_interface_go/README.md @@ -62,14 +62,14 @@ Pass a context with a deadline, or set `Options.CallTimeout` — otherwise `Defa ### `xbot.Dial(ctx, xbot.Options{...}) (*xbot.Conn, error)` -| field | meaning | -| ------------- | ------------------------------------------------------------------------------- | -| `ServiceID` | the id to connect to (required), see `service_ids.h` | -| `BindIP` | IPv4 address of the interface to use; `""`/`0.0.0.0` = all | -| `Heartbeat` | heartbeat interval; `0` = `xbot.DefaultHeartbeat` (1 s) | -| `NoClaim` | connect without claiming: the owner keeps the responses, this client only sends | -| `Endpoint` | `"ip:port"` — skip discovery (implies `NoClaim`) | -| `CallTimeout` | default timeout for `Call` without a context deadline; `0` = 2 s | +| field | meaning | +| ----------------------------- | -------------------------------------------------------------------------------- | +| `ServiceID` | the id to connect to (required), see `service_ids.h` | +| `BindIP` | IPv4 address of the interface to use; `""`/`0.0.0.0` = all | +| `Heartbeat` | heartbeat interval; `0` = `xbot.DefaultHeartbeat` (1 s) | +| `NoClaim` | connect without claiming: the owner keeps the responses, this client only sends | +| `Endpoint` | `"ip:port"` — skip discovery (implies `NoClaim`) | +| `CallTimeout` | default timeout for `Call` without a context deadline; `0` = 2 s | | `ExpectType`, `ExpectVersion` | validate the discovered service against the schema the client was generated from | Discovery needs a window of a few advertisement intervals: a service advertises every second only while it is @@ -97,12 +97,12 @@ descriptor. ### Errors -| error | meaning | -| ----------------------- | ------------------------------------------------------------------ | -| `xbot.ErrBusy` | the service is still executing another call (`errors.Is`) | -| `xbot.ErrRPC` | the call failed inside the service | -| `xbot.ErrNotAdvertised` | the service id was not advertised; the message lists what was seen | -| `*xbot.RPCError` | carries `FunctionID` and `Status`; matches the two above via `Is` | +| error | meaning | +| ----------------------------- | ---------------------------------------------------------------------------------------------- | +| `xbot.ErrBusy` | the service is still executing another call (`errors.Is`) | +| `xbot.ErrRPC` | the call failed inside the service | +| `xbot.ErrNotAdvertised` | the service id was not advertised; the message lists what was seen | +| `*xbot.RPCError` | carries `FunctionID` and `Status`; matches the two above via `Is` | | `xbot.ErrIncompatibleService` | the service announced a different type/version than the client expects (`*xbot.MismatchError`) | ### Discovery and helpers @@ -135,6 +135,29 @@ The output is `gofmt`ed and checked in (Go convention); a `go:generate` directiv `go generate ./... && git diff --exit-code` in CI keeps it in sync with the schema. Generator and golden tests live in `codegen/`. +**Naming.** The schema is the reference, the Go API follows the Go naming rules for initialisms: +`SoundId` -> `SoundID`, `GpsQuality` -> `GPSQuality`, `PlayMp3` -> `PlayMP3`, `I2cReceive` -> +`I2CReceive`, `GPS_RTK_FIX` -> `GPSRTKFix`. That translation is mechanical (see `codegen/names.go`, +pinned by `TestGoName`) and - because C++ style (`Gps`) and Go style (`GPS`) disagree by design - +the original name stays visible where it matters: + +```go +// PlayMP3 calls the RPC function 3 ("PlayMp3" in the service.json). +func (c *Client) PlayMP3(...) + +// SendGPSQuality sends the input 3 ("Gps Quality" in the service.json). +func (c *Client) SendGPSQuality(GPSQuality float32) error + +// SoundID is the service.json enum "SoundId". +type SoundID uint8 + +SoundIDBootPing SoundID = 0 // BOOT_PING +``` + +Doc comments carry it on types, RPC methods, register setters and input senders; enum values carry it as a trailing comment; the `Fn*` function-id constants have none, because they are only used with the +RPC method whose doc comment names the schema function. So `grep` on the schema name always finds the +Go identifier, and a reader can go from `service.json` to the client without knowing the rules. + ## Scope The client implements what a tool like `soundctl` needs: discovery, claim, heartbeat, RPC (synchronous and diff --git a/xbot_service_interface_go/codegen/emit.go b/xbot_service_interface_go/codegen/emit.go index b43c8df..baefc95 100644 --- a/xbot_service_interface_go/codegen/emit.go +++ b/xbot_service_interface_go/codegen/emit.go @@ -87,7 +87,7 @@ func (g *generator) goScalar(t, where string) (string, error) { return gt, nil } if g.enumBase(t) != "" { - return t, nil + return goName(t), nil } return "", fmt.Errorf("go codegen: unsupported type %q in %s", t, where) } diff --git a/xbot_service_interface_go/codegen/generate_test.go b/xbot_service_interface_go/codegen/generate_test.go index d839b39..c9d98e3 100644 --- a/xbot_service_interface_go/codegen/generate_test.go +++ b/xbot_service_interface_go/codegen/generate_test.go @@ -55,13 +55,25 @@ func TestGenerateGolden(t *testing.T) { // as well, instead of producing a client that cannot work. func TestLoadRejects(t *testing.T) { cases := map[string]string{ - "missing type": `{"version": 1}`, - "unknown type": `{"type": "X", "version": 1, "inputs": [{"id": 0, "name": "A", "type": "uint7_t"}]}`, - "blob as input": `{"type": "X", "version": 1, "inputs": [{"id": 0, "name": "A", "type": "blob"}]}`, - "duplicate ids": `{"type": "X", "version": 1, "inputs": [{"id": 1, "name": "A", "type": "uint8_t"}, {"id": 1, "name": "B", "type": "uint8_t"}]}`, - "name collision": `{"type": "X", "version": 1, "inputs": [{"id": 0, "name": "My Input", "type": "uint8_t"}, {"id": 1, "name": "MyInput", "type": "uint8_t"}]}`, - "unknown enum base": `{"type": "X", "version": 1, "enums": [{"id": "E", "base_type": "uint7_t", "values": {"A": 0}}]}`, - "empty enum": `{"type": "X", "version": 1, "enums": [{"id": "E", "base_type": "uint8_t", "to_string": true, "values": {}}]}`, + "missing type": `{"version": 1}`, + "unknown type": `{"type": "X", "version": 1, "inputs": [{"id": 0, "name": "A", "type": "uint7_t"}]}`, + "blob as input": `{"type": "X", "version": 1, "inputs": [{"id": 0, "name": "A", "type": "blob"}]}`, + "duplicate ids": `{"type": "X", "version": 1, "inputs": [{"id": 1, "name": "A", "type": "uint8_t"}, {"id": 1, "name": "B", "type": "uint8_t"}]}`, + "name collision": `{"type": "X", "version": 1, "inputs": [{"id": 0, "name": "My Input", "type": "uint8_t"}, {"id": 1, "name": "MyInput", "type": "uint8_t"}]}`, + "unknown enum base": `{"type": "X", "version": 1, "enums": [{"id": "E", "base_type": "uint7_t", "values": {"A": 0}}]}`, + "empty enum": `{"type": "X", "version": 1, "enums": [{"id": "E", "base_type": "uint8_t", "to_string": true, "values": {}}]}`, + "function shadows Client method": `{"type": "X", "version": 1, "functions": [{"id": 0, "name": "Close", "return_type": "void"}]}`, + "function and register share a setter": `{"type": "X", "version": 1, + "functions": [{"id": 0, "name": "SetVolume", "return_type": "void"}], + "registers": [{"id": 0, "name": "Volume", "type": "uint8_t"}]}`, + "functions normalize equal": `{"type": "X", "version": 1, "functions": [ + {"id": 0, "name": "SoundId", "return_type": "void"}, + {"id": 1, "name": "SoundID", "return_type": "void"}]}`, + "enum values normalize equal": `{"type": "X", "version": 1, "enums": [{"id": "E", "base_type": "uint8_t", + "values": {"IDLE": 0, "Idle": 1}}]}`, + "enum types normalize equal": `{"type": "X", "version": 1, "enums": [ + {"id": "SoundId", "base_type": "uint8_t", "values": {"A": 0}}, + {"id": "SoundID", "base_type": "uint8_t", "values": {"A": 0}}]}`, } for name, schema := range cases { t.Run(name, func(t *testing.T) { diff --git a/xbot_service_interface_go/codegen/names.go b/xbot_service_interface_go/codegen/names.go new file mode 100644 index 0000000..5d56e88 --- /dev/null +++ b/xbot_service_interface_go/codegen/names.go @@ -0,0 +1,138 @@ +package codegen + +import ( + "strings" + "unicode" + "unicode/utf8" +) + +// initialismNames lists the words Go writes in one case (ID, not Id; URL, not +// Url; Hz, not HZ), in their canonical spelling. The lookup map below is derived +// from it, keyed by the lower case spelling, so one entry covers CamelCase +// ("SoundId"), SCREAMING_SNAKE ("GPS_RTK_FIX") and prose ("State ID"). +var initialismNames = []string{ + "ADC", "API", "BMS", "CAN", "CDC", "ESC", "GPS", "GPIO", "HTTP", "Hz", + "I2C", "I2S", "ID", "IMU", "IP", "JSON", "LED", "MP3", "PCB", "PID", + "PWM", "ROS", "RPC", "RTK", "SPI", "TCP", "UDP", "URL", "USB", +} + +// initialisms maps the lower case spelling to the canonical one. +var initialisms = func() map[string]string { + m := make(map[string]string, len(initialismNames)) + for _, name := range initialismNames { + m[strings.ToLower(name)] = name + } + return m +}() + +// goName converts a schema identifier into a Go identifier conforming to the Go +// naming rules for initialisms: "SoundId" -> "SoundID", "PlayMp3" -> "PlayMP3", +// "I2cReceive" -> "I2CReceive", "GPS_RTK_FIX" -> "GPSRTKFix", "IDLE" -> "Idle". +// +// Names that need no change come out unchanged, so the generator output stays +// close to the service.json (which keeps it greppable against the C++ side). +func goName(name string) string { + words := splitWords(name) + if len(words) == 0 { + return "" + } + var b strings.Builder + for _, w := range words { + b.WriteString(goWord(w)) + } + return b.String() +} + +// splitWords splits on whitespace (spaces, tabs, newlines), underscores and +// dashes, and at CamelCase boundaries: "GPIOEvent" -> GPIO, Event; "I2cReceive" -> I2c, Receive; +// "Battery Voltage ADC" -> Battery, Voltage, ADC; "GPS_RTK_FIX" -> GPS, RTK, FIX. +func splitWords(name string) []string { + isSep := func(r rune) bool { return r == '_' || r == '-' || unicode.IsSpace(r) } + isUpper := func(r rune) bool { return r >= 'A' && r <= 'Z' } + isLower := func(r rune) bool { return r >= 'a' && r <= 'z' } + isDigit := func(r rune) bool { return r >= '0' && r <= '9' } + + runes := []rune(name) + var words []string + start := -1 + emit := func(end int) { + if start >= 0 && end > start { + words = append(words, string(runes[start:end])) + } + start = -1 + } + + for i := 0; i < len(runes); i++ { + r := runes[i] + if isSep(r) { + emit(i) + continue + } + if start < 0 { + start = i + continue + } + prev := runes[i-1] + switch { + case (isLower(prev) || isDigit(prev)) && isUpper(r): + // "SoundId": a new word starts at the upper case letter. + emit(i) + start = i + case isUpper(prev) && isUpper(r) && i+1 < len(runes) && isLower(runes[i+1]): + // "GPIOEvent": the last upper case letter starts the next word. + emit(i) + start = i + } + } + emit(len(runes)) + return words +} + +// goWord renders one word: known initialisms as they are, an all upper case run +// of initialisms split into them (GPIOID -> GPIOID), everything else Title case +// (IDLE -> Idle, FIX -> Fix). +func goWord(word string) string { + if mapped, ok := initialisms[strings.ToLower(word)]; ok { + return mapped + } + if word == strings.ToUpper(word) { + if parts, ok := splitInitialisms(word); ok { + return parts + } + } + // Title case, decoding the first rune so accented letters survive. + r, size := utf8.DecodeRuneInString(word) + if r == utf8.RuneError && size <= 1 { + return word + } + return string(unicode.ToUpper(r)) + strings.ToLower(word[size:]) +} + +// splitInitialisms splits an all upper case word greedily into initialisms +// ("GPIOID" -> "GPIO"+"ID"). It reports false unless the whole word is covered, +// so a normal word in capitals (IDLE) is not mangled into "ID"+"LE". +func splitInitialisms(word string) (string, bool) { + lower := strings.ToLower(word) + var b strings.Builder + parts := 0 + for pos := 0; pos < len(lower); { + best := "" + for candidate := range initialisms { + if len(candidate) > len(best) && strings.HasPrefix(lower[pos:], candidate) { + best = candidate + } + } + if best == "" { + return "", false + } + b.WriteString(initialisms[best]) + pos += len(best) + parts++ + } + // A word that is a single initialism is already handled by the table lookup + // in goWord, so only report a real split. + if parts < 2 { + return "", false + } + return b.String(), true +} diff --git a/xbot_service_interface_go/codegen/names_test.go b/xbot_service_interface_go/codegen/names_test.go new file mode 100644 index 0000000..8b96522 --- /dev/null +++ b/xbot_service_interface_go/codegen/names_test.go @@ -0,0 +1,58 @@ +package codegen + +import "testing" + +// TestGoName pins the naming rules against the identifiers that actually occur in +// the OpenMower service definitions plus the tricky shapes (all caps runs, +// digits, prose with spaces). +func TestGoName(t *testing.T) { + cases := map[string]string{ + // initialisms in CamelCase + "SoundId": "SoundID", + "PlayMp3": "PlayMP3", + "BusID": "BusID", + "GPIOID": "GPIOID", + "GpsQuality": "GPSQuality", + "I2cReceive": "I2CReceive", + "I2cTransmit": "I2CTransmit", + "I2cResult": "I2CResult", + "BatteryVoltageADC": "BatteryVoltageADC", + "ReadGPIO": "ReadGPIO", + "SubscribeGPIO": "SubscribeGPIO", + "GPIOEventFlags": "GPIOEventFlags", + // constant style: SCREAMING_SNAKE + "GPS_RTK_FIX": "GPSRTKFix", + "ROS_CONNECTED": "ROSConnected", + "RosDisconnected": "ROSDisconnected", + "EscStatus": "ESCStatus", + "GPS_RTK_LOST": "GPSRTKLost", + "BOOT_PING": "BootPing", + "IDLE": "Idle", + "MP3": "MP3", + "TONE": "Tone", + // prose names from the registers + "State ID": "StateID", + "Gps Quality": "GPSQuality", + "GPIO Configs": "GPIOConfigs", + "Battery Voltage ADC": "BatteryVoltageADC", + "Charger Enabled": "ChargerEnabled", + "Sound Definitions": "SoundDefinitions", + "Simulated Inputs": "SimulatedInputs", + // any whitespace separates words, not just the space + "Battery\tVoltage": "BatteryVoltage", + "Sound\nDefinitions": "SoundDefinitions", + "Gps Quality": "GPSQuality", + // untouched names + "Path": "Path", + "Hash": "Hash", + "DurationMs": "DurationMs", + "DetuneHz": "DetuneHz", + "FileExists": "FileExists", + "Waveform": "Waveform", + } + for in, want := range cases { + if got := goName(in); got != want { + t.Errorf("goName(%q) = %q, want %q", in, got, want) + } + } +} diff --git a/xbot_service_interface_go/codegen/render.go b/xbot_service_interface_go/codegen/render.go index c1a7f42..2e012f2 100644 --- a/xbot_service_interface_go/codegen/render.go +++ b/xbot_service_interface_go/codegen/render.go @@ -5,6 +5,15 @@ import ( "strings" ) +// schemaRef appends the name as written in the service.json when it differs from +// the generated Go name, so the schema stays the reference a reader greps for. +func schemaRef(raw, goName string) string { + if raw == goName { + return "" + } + return fmt.Sprintf(" (%q in the service.json)", raw) +} + // run emits the whole file: header, ids, enums, the Client, one method per RPC // function, register setters, input senders and the wire helpers. func (g *generator) run() error { @@ -39,7 +48,7 @@ func (g *generator) run() error { g.line(`// Function ids (service.json -> "functions").`) g.line("const (") for _, fn := range s.Functions { - g.f("\tFn%s uint8 = %d", normalizeName(fn.Name), fn.ID) + g.f("\tFn%s uint8 = %d", goName(fn.Name), fn.ID) } g.line(")") g.line("") @@ -107,8 +116,13 @@ func (g *generator) emitEnum(e Enum) error { if !ok { return fmt.Errorf("go codegen: enum %s: unknown base_type %q", e.ID, e.BaseType) } - g.f("// %s values (service.json -> \"enums\").", e.ID) - g.f("type %s %s", e.ID, base) + typeName := goName(e.ID) + if e.ID != typeName { + g.f("// %s is the service.json enum %q.", typeName, e.ID) + } else { + g.f("// %s values (service.json -> \"enums\").", typeName) + } + g.f("type %s %s", typeName, base) g.line("") g.line("const (") for _, name := range sortedEnumValues(e) { @@ -116,7 +130,13 @@ func (g *generator) emitEnum(e Enum) error { if e.Bitmask { lit = "1 << " + lit } - g.f("\t%s%s %s = %s", e.ID, name, e.ID, lit) + constName := goName(name) + line := fmt.Sprintf("\t%s%s %s = %s", typeName, constName, typeName, lit) + if constName != name { + // keep the value name of the service.json next to the constant + line += " // " + name + } + g.line(line) } g.line(")") g.line("") @@ -124,7 +144,7 @@ func (g *generator) emitEnum(e Enum) error { return nil } g.line("// String returns the name of the value.") - g.f("func (v %s) String() string {", e.ID) + g.f("func (v %s) String() string {", typeName) g.line("\tswitch v {") seen := map[int64]bool{} for _, name := range sortedEnumValues(e) { @@ -132,7 +152,7 @@ func (g *generator) emitEnum(e Enum) error { continue } seen[e.Values[name]] = true - g.f("\tcase %s%s:", e.ID, name) + g.f("\tcase %s%s:", typeName, goName(name)) g.f("\t\treturn %q", strings.ToLower(name)) } g.line("\t}") @@ -144,11 +164,11 @@ func (g *generator) emitEnum(e Enum) error { // emitFunction writes one RPC method. func (g *generator) emitFunction(fn Function) error { - name := normalizeName(fn.Name) + name := goName(fn.Name) sig := []string{"ctx context.Context"} var args []string for _, p := range fn.Parameters { - pname := normalizeName(p.Name) + pname := goName(p.Name) where := "parameter " + pname + " of " + name gt, err := g.goType(p.Type, where) if err != nil { @@ -170,7 +190,7 @@ func (g *generator) emitFunction(fn Function) error { ret = "(" + retType + ", error)" } - g.f("// %s calls the RPC function %d.", name, fn.ID) + g.f("// %s calls the RPC function %d%s.", name, fn.ID, schemaRef(fn.Name, name)) g.f("func (c *Client) %s(%s) %s {", name, strings.Join(sig, ", "), ret) assign := "\tpayload, err := " if retType == "" { @@ -203,7 +223,7 @@ func (g *generator) emitFunction(fn Function) error { // emitRegister writes a setter that sends the register as a configuration // transaction (the service reconfigures and persists it). func (g *generator) emitRegister(r Field) error { - name := normalizeName(r.Name) + name := goName(r.Name) where := "register " + name gt, err := g.goType(r.Type, where) if err != nil { @@ -213,8 +233,8 @@ func (g *generator) emitRegister(r Field) error { if err != nil { return err } - g.f("// Set%s writes the register %d (\"%s\") as a configuration transaction.", - name, r.ID, name) + g.f("// Set%s writes the register %d%s as a configuration transaction.", + name, r.ID, schemaRef(r.Name, name)) g.f("func (c *Client) Set%s(%s %s) error {", name, name, gt) g.f("\treturn c.conn.SendTransaction(true, xbot.Data{ID: %d, Data: %s})", r.ID, expr) g.line("}") @@ -224,7 +244,7 @@ func (g *generator) emitRegister(r Field) error { // emitInput writes an input sender. func (g *generator) emitInput(in Field) error { - name := normalizeName(in.Name) + name := goName(in.Name) where := "input " + name gt, err := g.goType(in.Type, where) if err != nil { @@ -234,7 +254,7 @@ func (g *generator) emitInput(in Field) error { if err != nil { return err } - g.f("// Send%s sends the input %d (\"%s\").", name, in.ID, name) + g.f("// Send%s sends the input %d%s.", name, in.ID, schemaRef(in.Name, name)) g.f("func (c *Client) Send%s(%s %s) error {", name, name, gt) g.f("\treturn c.conn.SendData(%d, %s)", in.ID, expr) g.line("}") diff --git a/xbot_service_interface_go/codegen/schema.go b/xbot_service_interface_go/codegen/schema.go index 006c86c..c70f2ad 100644 --- a/xbot_service_interface_go/codegen/schema.go +++ b/xbot_service_interface_go/codegen/schema.go @@ -13,7 +13,6 @@ import ( "os" "regexp" "sort" - "strings" ) // Schema is a service.json. @@ -153,7 +152,7 @@ func (s *Schema) validate() error { } } } - return nil + return s.validateGeneratedNames() } // checkUniqueIDs rejects duplicate ids and duplicate normalized names, which the @@ -183,7 +182,7 @@ func addUnique(ids map[int]bool, names map[string]string, what string, id int, n return fmt.Errorf("%s: duplicate id %d", what, id) } ids[id] = true - normalized := normalizeName(name) + normalized := goName(name) if other, ok := names[normalized]; ok { return fmt.Errorf("%s: %q and %q normalize to the same Go name %q", what, other, name, normalized) } @@ -191,12 +190,6 @@ func addUnique(ids map[int]bool, names map[string]string, what string, id int, n return nil } -// normalizeName removes whitespace, exactly like xbot_codegen.toCamelCase: the -// generated method name for "File Exists" is "FileExists". -func normalizeName(name string) string { - return strings.Join(strings.Fields(name), "") -} - // splitType splits "char[128]" into ("char", 128, true). func splitType(t string) (base string, length int, isArray bool) { if m := arrayType.FindStringSubmatch(t); m != nil { diff --git a/xbot_service_interface_go/codegen/testdata/demo_service.golden.go b/xbot_service_interface_go/codegen/testdata/demo_service.golden.go index d190153..2524ef9 100644 --- a/xbot_service_interface_go/codegen/testdata/demo_service.golden.go +++ b/xbot_service_interface_go/codegen/testdata/demo_service.golden.go @@ -31,18 +31,18 @@ const ( type ExampleEnumClass uint8 const ( - ExampleEnumClassVALUE1 ExampleEnumClass = 0 - ExampleEnumClassVALUE2 ExampleEnumClass = 1 - ExampleEnumClassVALUE3 ExampleEnumClass = 2 + ExampleEnumClassValue1 ExampleEnumClass = 0 // VALUE1 + ExampleEnumClassValue2 ExampleEnumClass = 1 // VALUE2 + ExampleEnumClassValue3 ExampleEnumClass = 2 // VALUE3 ) // ExampleBitmaskEnum values (service.json -> "enums"). type ExampleBitmaskEnum uint8 const ( - ExampleBitmaskEnumVALUE1 ExampleBitmaskEnum = 1 << 0 - ExampleBitmaskEnumVALUE2 ExampleBitmaskEnum = 1 << 1 - ExampleBitmaskEnumVALUE3 ExampleBitmaskEnum = 1 << 2 + ExampleBitmaskEnumValue1 ExampleBitmaskEnum = 1 << 0 // VALUE1 + ExampleBitmaskEnumValue2 ExampleBitmaskEnum = 1 << 1 // VALUE2 + ExampleBitmaskEnumValue3 ExampleBitmaskEnum = 1 << 2 // VALUE3 ) // Client is a client for the ServiceTemplate service. @@ -111,32 +111,32 @@ func (c *Client) MixedParamsWithReturn(ctx context.Context, Name string, Value f return payload[0], nil } -// SetRegister1 writes the register 0 ("Register1") as a configuration transaction. +// SetRegister1 writes the register 0 as a configuration transaction. func (c *Client) SetRegister1(Register1 string) error { return c.conn.SendTransaction(true, xbot.Data{ID: 0, Data: charParam(Register1, 42)}) } -// SetRegister2 writes the register 1 ("Register2") as a configuration transaction. +// SetRegister2 writes the register 1 as a configuration transaction. func (c *Client) SetRegister2(Register2 uint32) error { return c.conn.SendTransaction(true, xbot.Data{ID: 1, Data: leUint32(Register2)}) } -// SetRegister3 writes the register 2 ("Register3") as a configuration transaction. +// SetRegister3 writes the register 2 as a configuration transaction. func (c *Client) SetRegister3(Register3 []byte) error { return c.conn.SendTransaction(true, xbot.Data{ID: 2, Data: Register3}) } -// SetRegister4Optional writes the register 3 ("Register4Optional") as a configuration transaction. +// SetRegister4Optional writes the register 3 ("Register4 Optional" in the service.json) as a configuration transaction. func (c *Client) SetRegister4Optional(Register4Optional uint32) error { return c.conn.SendTransaction(true, xbot.Data{ID: 3, Data: leUint32(Register4Optional)}) } -// SendExampleInput1 sends the input 0 ("ExampleInput1"). +// SendExampleInput1 sends the input 0. func (c *Client) SendExampleInput1(ExampleInput1 string) error { return c.conn.SendData(0, charParam(ExampleInput1, 100)) } -// SendExampleInput2 sends the input 1 ("ExampleInput2"). +// SendExampleInput2 sends the input 1. func (c *Client) SendExampleInput2(ExampleInput2 uint32) error { return c.conn.SendData(1, leUint32(ExampleInput2)) } diff --git a/xbot_service_interface_go/codegen/testdata/demo_service_noid.golden.go b/xbot_service_interface_go/codegen/testdata/demo_service_noid.golden.go index 8769665..7f7821a 100644 --- a/xbot_service_interface_go/codegen/testdata/demo_service_noid.golden.go +++ b/xbot_service_interface_go/codegen/testdata/demo_service_noid.golden.go @@ -28,18 +28,18 @@ const ( type ExampleEnumClass uint8 const ( - ExampleEnumClassVALUE1 ExampleEnumClass = 0 - ExampleEnumClassVALUE2 ExampleEnumClass = 1 - ExampleEnumClassVALUE3 ExampleEnumClass = 2 + ExampleEnumClassValue1 ExampleEnumClass = 0 // VALUE1 + ExampleEnumClassValue2 ExampleEnumClass = 1 // VALUE2 + ExampleEnumClassValue3 ExampleEnumClass = 2 // VALUE3 ) // ExampleBitmaskEnum values (service.json -> "enums"). type ExampleBitmaskEnum uint8 const ( - ExampleBitmaskEnumVALUE1 ExampleBitmaskEnum = 1 << 0 - ExampleBitmaskEnumVALUE2 ExampleBitmaskEnum = 1 << 1 - ExampleBitmaskEnumVALUE3 ExampleBitmaskEnum = 1 << 2 + ExampleBitmaskEnumValue1 ExampleBitmaskEnum = 1 << 0 // VALUE1 + ExampleBitmaskEnumValue2 ExampleBitmaskEnum = 1 << 1 // VALUE2 + ExampleBitmaskEnumValue3 ExampleBitmaskEnum = 1 << 2 // VALUE3 ) // Client is a client for the ServiceTemplate service. @@ -108,32 +108,32 @@ func (c *Client) MixedParamsWithReturn(ctx context.Context, Name string, Value f return payload[0], nil } -// SetRegister1 writes the register 0 ("Register1") as a configuration transaction. +// SetRegister1 writes the register 0 as a configuration transaction. func (c *Client) SetRegister1(Register1 string) error { return c.conn.SendTransaction(true, xbot.Data{ID: 0, Data: charParam(Register1, 42)}) } -// SetRegister2 writes the register 1 ("Register2") as a configuration transaction. +// SetRegister2 writes the register 1 as a configuration transaction. func (c *Client) SetRegister2(Register2 uint32) error { return c.conn.SendTransaction(true, xbot.Data{ID: 1, Data: leUint32(Register2)}) } -// SetRegister3 writes the register 2 ("Register3") as a configuration transaction. +// SetRegister3 writes the register 2 as a configuration transaction. func (c *Client) SetRegister3(Register3 []byte) error { return c.conn.SendTransaction(true, xbot.Data{ID: 2, Data: Register3}) } -// SetRegister4Optional writes the register 3 ("Register4Optional") as a configuration transaction. +// SetRegister4Optional writes the register 3 ("Register4 Optional" in the service.json) as a configuration transaction. func (c *Client) SetRegister4Optional(Register4Optional uint32) error { return c.conn.SendTransaction(true, xbot.Data{ID: 3, Data: leUint32(Register4Optional)}) } -// SendExampleInput1 sends the input 0 ("ExampleInput1"). +// SendExampleInput1 sends the input 0. func (c *Client) SendExampleInput1(ExampleInput1 string) error { return c.conn.SendData(0, charParam(ExampleInput1, 100)) } -// SendExampleInput2 sends the input 1 ("ExampleInput2"). +// SendExampleInput2 sends the input 1. func (c *Client) SendExampleInput2(ExampleInput2 uint32) error { return c.conn.SendData(1, leUint32(ExampleInput2)) } diff --git a/xbot_service_interface_go/codegen/validate_names.go b/xbot_service_interface_go/codegen/validate_names.go new file mode 100644 index 0000000..c0545fe --- /dev/null +++ b/xbot_service_interface_go/codegen/validate_names.go @@ -0,0 +1,78 @@ +package codegen + +import "fmt" + +// fixedPackageNames are the package level identifiers every generated file has; +// a schema name must not shadow them. ServiceID is reserved even when no +// -service-id is passed, so a schema stays valid for both invocations. +var fixedPackageNames = []string{ + "Client", "Dial", "ServiceID", "errf", + "leUint8", "leUint16", "leUint32", "leUint64", + "leInt8", "leInt16", "leInt32", "leInt64", "leFloat32", "leFloat64", + "byteParam", "charParam", "charValue", +} + +// fixedClientMethods are the methods every generated Client has. +var fixedClientMethods = []string{"Close", "Endpoint"} + +// validateGeneratedNames rejects schemas whose generated Go identifiers would +// collide, before any code is emitted. Two namespaces matter: +// +// - package level: the Fn* function-id constants, the enum types and their +// values, and the generated helpers; +// - the Client: one method per function, register setter and input sender, +// plus the fixed Close and Endpoint. +// +// The comparison uses the same goName rules as the emitters, so a collision that +// only appears after the translation (IDLE and Idle, SoundId and SoundID) is +// caught as well. +func (s *Schema) validateGeneratedNames() error { + pkg := make(map[string]string, len(fixedPackageNames)+len(s.Functions)) + for _, name := range fixedPackageNames { + pkg[name] = "the generator" + } + method := make(map[string]string, len(fixedClientMethods)) + for _, name := range fixedClientMethods { + method[name] = "the generator" + } + + reserve := func(m map[string]string, name, what string) error { + if prev, ok := m[name]; ok { + return fmt.Errorf("%s and %s both generate the Go name %q", prev, what, name) + } + m[name] = what + return nil + } + + for _, fn := range s.Functions { + name := goName(fn.Name) + if err := reserve(pkg, "Fn"+name, "function "+fn.Name); err != nil { + return err + } + if err := reserve(method, name, "function "+fn.Name); err != nil { + return err + } + } + for _, r := range s.Registers { + if err := reserve(method, "Set"+goName(r.Name), "register "+r.Name); err != nil { + return err + } + } + for _, in := range s.Inputs { + if err := reserve(method, "Send"+goName(in.Name), "input "+in.Name); err != nil { + return err + } + } + for _, e := range s.Enums { + typeName := goName(e.ID) + if err := reserve(pkg, typeName, "enum "+e.ID); err != nil { + return err + } + for _, value := range sortedEnumValues(e) { + if err := reserve(pkg, typeName+goName(value), "value "+value+" of enum "+e.ID); err != nil { + return err + } + } + } + return nil +}