Skip to content

Commit 3b3e01a

Browse files
committed
docs(gateway): correct schema-generation description (opaque DTOs, no merged survivors)
The OpenAPI tutorial and the DTO contract design doc overstated the field-walking path: - components/schemas is now exactly collect_component_schemas(); the from_ros_msg / *_srv_* / binary_schema / generic_object_schema factories are no longer merged into it. They feed inline path-operation schemas for the per-topic / per-service / per-action routes whose shape comes from the live ROS 2 type, not from a named DTO. - The Data*Result / Fault*Result / OperationExecutionResult opaque DTOs have no dto_fields; they carry a hand-written JsonWriter/JsonReader/SchemaWriter trio and publish an opaque object schema (type: object, additionalProperties: true, x-medkit-opaque: true) rather than a field-walked struct or a bare {}.
1 parent 10c2f36 commit 3b3e01a

2 files changed

Lines changed: 48 additions & 38 deletions

File tree

docs/tutorials/openapi.rst

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -81,20 +81,29 @@ How Schemas Are Generated
8181
--------------------------
8282

8383
The ``components/schemas`` object in every ``/docs`` response is generated
84-
automatically from the DTO registry. Each response and request type in the
85-
gateway is declared as a plain C++ struct with a ``constexpr dto_fields<T>``
86-
descriptor tuple. The ``SchemaWriter<T>`` visitor folds over this tuple at
87-
compile time to produce the OpenAPI JSON Schema entry, and the
88-
``AllDtos`` registry in ``dto/registry.hpp`` lists every named type so that
89-
``collect_component_schemas()`` can populate the full schema map without
90-
any hand-written schema factories.
91-
92-
The same descriptor is used for serialization (``JsonWriter<T>``) and
93-
request-body validation (``JsonReader<T>``), so the wire shape and the
94-
published schema are always derived from the same source. Genuinely dynamic
95-
payloads - such as live ROS 2 message data and free-form fault environment
96-
records - are typed as ``nlohmann::json`` members and appear in the schema
97-
as unconstrained objects (``{}``).
84+
automatically from the DTO registry. Most response and request types are
85+
declared as a plain C++ struct with a ``constexpr dto_fields<T>`` descriptor
86+
tuple; the ``SchemaWriter<T>`` visitor folds over this tuple at compile time to
87+
produce the OpenAPI JSON Schema entry. The ``AllDtos`` registry in
88+
``dto/registry.hpp`` lists every named type so that
89+
``collect_component_schemas()`` populates the entire ``components/schemas`` map
90+
with no hand-written schema factories.
91+
92+
For a field-walking DTO the same descriptor also drives serialization
93+
(``JsonWriter<T>``) and request-body validation (``JsonReader<T>``), so the wire
94+
shape and the published schema are always derived from the same source.
95+
96+
A few envelope types whose payload shape is decided at runtime by a plugin or by
97+
a live ROS 2 type - the ``Data*Result`` / ``Fault*Result`` /
98+
``OperationExecutionResult`` *opaque* DTOs - have no ``dto_fields``. They carry a
99+
hand-written ``JsonWriter`` / ``JsonReader`` / ``SchemaWriter`` trio instead, are
100+
still listed in ``AllDtos``, and publish an opaque object schema
101+
(``{type: object, additionalProperties: true, x-medkit-opaque: true}``). Inside
102+
an ordinary DTO, a free-form member typed as a bare ``nlohmann::json`` (for
103+
example the fault environment records) appears as an unconstrained object
104+
(``{}``). The per-topic / per-service / per-action routes for live ROS 2 data do
105+
not use ``components/schemas`` at all: their request and response bodies carry an
106+
inline schema derived from the ROS 2 type on the route itself.
98107

99108
For the full design of the DTO contract layer, see
100109
:doc:`/design/ros2_medkit_gateway/dto_contract`.

src/ros2_medkit_gateway/design/dto_contract.rst

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -268,20 +268,19 @@ AllDtos Registry (``registry.hpp``)
268268
269269
The free function ``collect_component_schemas()`` iterates ``AllDtos`` at
270270
compile time (via ``std::index_sequence``) and calls
271-
``SchemaWriter<T>::schema()`` for each type, producing the bulk of the
272-
``components/schemas`` map. ``SchemaBuilder::component_schemas()`` (in
273-
``src/openapi/schema_builder.cpp``) merges these DTO-generated entries with a
274-
small number of explicitly hand-written survivors:
275-
276-
- **``from_ros_msg`` / ``from_ros_srv_request`` / ``from_ros_srv_response``** -
277-
schema factories for dynamic ROS 2 payloads whose field names are not known
278-
at compile time (topic samples, service request/response bodies).
279-
- **``binary_schema``** and **``generic_object_schema``** - trivial inline
280-
schema objects used for bulk-data and free-form fields.
281-
282-
With the exception of these survivors, every schema in ``components/schemas``
283-
is generated from ``AllDtos``. No runtime loop over a dynamic registry is
284-
required.
271+
``SchemaWriter<T>::schema()`` for each type. ``SchemaBuilder::component_schemas()``
272+
(in ``src/openapi/schema_builder.cpp``) returns exactly this map: every entry in
273+
``components/schemas`` is generated from ``AllDtos``, with no hand-written
274+
survivors merged in. No runtime loop over a dynamic registry is required.
275+
276+
The hand-written schema factories that remain on ``SchemaBuilder`` -
277+
``from_ros_msg`` / ``from_ros_srv_request`` / ``from_ros_srv_response`` (for
278+
dynamic ROS 2 payloads whose field names are not known at compile time) and
279+
``binary_schema`` / ``generic_object_schema`` - are no longer part of the
280+
``components/schemas`` map. They are called by the path builder
281+
(``src/openapi/path_builder.cpp``) to emit *inline* operation schemas for the
282+
per-topic / per-service / per-action routes, whose request and response shape is
283+
derived from the live ROS 2 type rather than from a named DTO.
285284

286285
The ``Collection<T>`` template is a generic DTO for paginated list responses
287286
(``{"items": [...]}``). It is specialized for each element type in
@@ -539,23 +538,25 @@ OpenAPI Generation Pipeline
539538

540539
The published ``openapi.json`` is assembled mechanically from two sources:
541540

542-
- ``components/schemas`` is the union of
543-
``collect_component_schemas<AllDtos>()`` (one entry per DTO listed in
544-
``dto/registry.hpp``) and a small set of explicit survivors in
545-
``SchemaBuilder::component_schemas()`` for genuinely dynamic ROS payloads
546-
(``from_ros_msg`` / ``from_ros_srv_request`` / ``from_ros_srv_response``,
547-
``binary_schema``, ``generic_object_schema``).
541+
- ``components/schemas`` is exactly ``collect_component_schemas<AllDtos>()`` -
542+
one entry per DTO listed in ``dto/registry.hpp``, with no hand-written
543+
survivors merged in.
548544
- ``paths`` is ``RouteRegistry::to_openapi_paths()``: every typed route
549545
contributes a path item with ``$ref`` entries auto-derived from its
550546
``TResponse`` / ``TBody`` template parameters plus any tags, summary,
551547
description, ``operation_id``, parameter, or extra-status metadata pinned
552-
on the route via the fluent ``RouteEntry`` builder.
548+
on the route via the fluent ``RouteEntry`` builder. The per-topic /
549+
per-service / per-action routes for genuinely dynamic ROS 2 payloads carry an
550+
*inline* schema built by ``SchemaBuilder``'s ``from_ros_msg`` /
551+
``from_ros_srv_request`` / ``from_ros_srv_response`` / ``binary_schema`` /
552+
``generic_object_schema`` factories (these feed path operations, not
553+
``components/schemas``).
553554

554555
``OpenApiSpecBuilder::build()`` then assembles ``info`` / ``servers`` /
555556
``tags`` / ``security`` around those two compiled blocks. There are no
556-
hand-written ``paths`` items in the published spec, and no hand-written
557-
schema blocks beyond the survivors above. Adding a route or a DTO field
558-
updates the spec on the next process start with no schema-side edit.
557+
hand-written ``paths`` items in the published spec, and no hand-written schema
558+
blocks in ``components/schemas``. Adding a route or a DTO field updates the
559+
spec on the next process start with no schema-side edit.
559560

560561
Optional fields are now emitted as ``anyOf: [<inner>, {type: "null"}]``
561562
(OpenAPI 3.1 idiom) so generated clients see ``T | null`` rather than

0 commit comments

Comments
 (0)