Files
logos-cpp-sdk/cpp-generator/experimental
Dario Gabriel LipicarandClaude Opus 5 22aa17c129 feat(records): a struct in the impl header is a real wire type
`any` (LogosMap/LogosList) was the only way to express a heterogeneous shape, so
~104 slots across openmetrics, package_manager, package_downloader, storage and
logoscore-cli describe their payloads with an untyped map. LIDL has carried
TypeDecl/FieldDecl all along — chat_module's hand-written .lidl declares records
and notes they "document the shape and light up if the generators gain struct
support". This is the impl-header path finally producing them.

  parser  - a plain `struct Foo { T a; U b; };` ahead of the impl class becomes a
            TypeDecl. Field types go through the same cppTypeToLidl, so a record
            field is subject to the same 64-bit / bstr / composition rules.
  gate    - a Named type is supported when the module DECLARES it. An undeclared
            name is still a build error naming the type: records are opt-in, not
            a reopening of the opaque fallback.
  emitter - one Codec<Foo> specialisation per record. Fields are addressed
            through decltype, so no C++ type name is spelled (same trick as
            JsonArg), and because the specialisation plugs into
            logos::detail::Codec, a record nested in [T] or {tstr: T} needs
            nothing further emitted — the existing recursion handles it.

A missing field decodes as null and the leaf codec rejects it with the field's
path, so the diagnostics compose too.

Verified end to end, not just as emitted text — a module declaring
`struct Status { uint64_t port; std::string name; std::vector<uint8_t> blob; }`
used as param, return and inside a vector, over the real transport:

  param        {"port":9099,"name":"ok","blob":{"_bytes":"gAE"}} -> "9099|ok|2:128:1:129"
  return       {"blob":{"_bytes":"gAE"},"name":"ok","port":9099}
  [record]     [{...},{"blob":{"_bytes":""},"name":"b","port":1}]
  missing field {"code":"dispatch_failed",
                 "message":"expected string at arg0.name, got null"}

Note the bytes field: tagged inside the record, and inside a record inside an
array, with no record-specific bytes handling anywhere.

Tests: 175/175, including that an undeclared Named type is still rejected.

Next for records: emit the `type Foo {...}` decls into the published .lidl so
consumers generate typed wrappers, then the Rust and Qt sides. Until then a
record is a provider-side contract that consumers still see as an object.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 15:05:58 -03:00
..