Skip to content

Commit a17938c

Browse files
committed
test+docs(gateway): plugin ABI lock, TSan suppressions, design doc, CHANGELOG
- test_plugin_abi_conformance.cpp: loads test_gateway_plugin.so via the production PluginLoader and verifies the plugin ABI is byte-stable. Twelve compile-time static_asserts guard the public layout of GatewayPlugin, PluginRoute, PluginRequest, PluginResponse; six runtime tests cover route registration, send_json content-type/indent, send_error vendor-error remapping and status clamping. If a future change breaks the plugin contract this test fails before commercial plugin builds catch it. - typed_test_fixture.hpp: shared TypedRequest synthesis helper for the handler test suites that exercise the typed handlers directly. - tsan_suppressions.txt: narrowly-scoped suppressions for libstdc++ future internals, yaml-cpp, vendored dynmsg, and the libyaml-cpp shared object. These races trace into third-party code reached via rclcpp GenericClient / dynamic ROS type introspection; our own JSON/transport paths remain visible to TSan. - design/dto_contract.rst: typed router section, escape hatch enumeration, provider ABI typed-only policy, fan-out observability via peer_dropped_items, opaque object policy, OpenAPI generation pipeline. - CHANGELOG.rst, README.md, docs/tutorials/openapi.rst: BREAKING entries on HandlerContext::send_* removal, provider ABI typed returns, RouteRegistry raw overload deletion, and the OpenAPI 3.1 anyOf+null representation for optional fields. - test_openapi_callability.test.py: parametrized x-medkit presence test across all four entity types (Area / Component / App / Function) and updated optional-field probes to walk the new anyOf shape.
1 parent 84252cb commit a17938c

9 files changed

Lines changed: 1547 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ Beyond faults, medkit exposes the full ROS 2 graph through REST:
9898
| **Software Updates** | Async prepare/execute lifecycle with pluggable backends |
9999
| **Authentication** | JWT-based RBAC (viewer, operator, configurator, admin) |
100100
| **Logs** | Log entries and configuration |
101-
| **Docs** | OpenAPI 3.1.0 spec and Swagger UI at `/api/v1/docs` |
101+
| **Docs** | OpenAPI 3.1.0 spec and Swagger UI at `/api/v1/docs` - schemas are generated from typed C++ structs so the spec always matches the wire format |
102102

103103
On the [roadmap](https://selfpatch.github.io/ros2_medkit/roadmap.html): entity lifecycle control, mode management, communication logs.
104104

docs/tutorials/openapi.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,28 @@ When disabled, all ``/docs`` endpoints return HTTP 501.
7777

7878
See :doc:`/config/server` for the full parameter reference.
7979

80+
How Schemas Are Generated
81+
--------------------------
82+
83+
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 (``{}``).
98+
99+
For the full design of the DTO contract layer, see
100+
:doc:`/design/ros2_medkit_gateway/dto_contract`.
101+
80102
See Also
81103
--------
82104

src/ros2_medkit_gateway/CHANGELOG.rst

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,56 @@ Unreleased
77

88
**Breaking Changes:**
99

10+
* Typed router refactor. ``HandlerContext`` no longer carries
11+
``send_json`` / ``send_error`` / ``send_plugin_error`` / ``send_dto`` /
12+
``parse_body``: handlers return ``http::Result<TResponse>`` and the
13+
framework owns response writing through ``RouteRegistry``. The raw
14+
``void(httplib::Request, httplib::Response)`` ``RouteRegistry`` lambda
15+
overloads are removed - call sites must use the typed
16+
``reg.get<T>`` / ``reg.post<TBody, T>`` / ``reg.del<T>`` overloads, the
17+
multi-shape ``reg.post_alternates<TBody, TAlt...>`` /
18+
``reg.del_alternates<TAlt...>``, or one of the named escape hatches
19+
(``reg.sse`` / ``reg.binary_download`` / ``reg.multipart_upload<T>`` /
20+
``reg.static_asset`` / ``reg.docs_endpoint`` / ``reg.docs_subtree``).
21+
``static_assert(dto::has_dto_shape_v<T>)`` gates every typed overload, so
22+
non-DTO return types fail at compile time. The plugin ABI is unaffected:
23+
``PluginResponse`` keeps its ``send_json`` / ``send_error`` surface and
24+
now routes through the same internal ``http::detail::write_json_body``
25+
primitive as the framework, so plugin wire format is unchanged
26+
(`#403 <https://github.com/selfpatch/ros2_medkit/issues/403>`_)
27+
* Provider ABI typed. ``FaultProvider``, ``DataProvider``,
28+
``OperationProvider``, and ``UpdateProvider::get_update`` return typed
29+
DTO envelopes (``FaultListResult`` / ``FaultDetailResult`` /
30+
``FaultClearResult`` / the matching ``Data*Result`` and
31+
``Operation*Result`` shapes / ``UpdateStatusResult``) instead of raw
32+
``tl::expected<nlohmann::json, ErrorInfo>``. The wire bytes are
33+
byte-identical because each envelope wraps an opaque ``content`` object
34+
emitted verbatim by ``JsonWriter``; commercial and out-of-tree plugins
35+
must wrap their existing JSON in the matching envelope type
36+
(mechanical: ``Result.content = std::move(json_payload)``). The plugin
37+
ABI itself (``PluginRoute`` shape, ``PluginResponse`` ctor, plugin api
38+
version) is locked by ``test_plugin_abi_conformance`` and is unchanged
39+
(`#403 <https://github.com/selfpatch/ros2_medkit/issues/403>`_)
40+
* ``SchemaWriter`` emits optional DTO fields as
41+
``anyOf: [<inner>, {type: "null"}]`` (the OpenAPI 3.1 idiom) instead of
42+
``nullable: true``. Generated clients see ``T | null`` for every optional
43+
field rather than ``T | undefined``. Wire format is unchanged - the
44+
gateway still omits absent optional fields, and ``JsonReader`` continues
45+
to accept absent fields; the schema change only opts the published spec
46+
into round-tripping a literal ``null`` value cleanly for clients that
47+
prefer to send one. The ``rtmaps_medkit`` variant is explicitly NOT
48+
covered by this PR - its handlers continue to run on the pre-typed
49+
HandlerContext surface and will be migrated separately
50+
(`#403 <https://github.com/selfpatch/ros2_medkit/issues/403>`_)
1051
* ``ros2_medkit_msgs/srv/ClearFault`` request gains a ``bool skip_correlation_auto_clear`` field (see the per-entity fault scope entry below for the in-tree motivation). Adding a request field changes the service type hash, so out-of-tree callers that invoke the service directly (for example ``ros2 service call /fault_manager/clear_fault ros2_medkit_msgs/srv/ClearFault ...`` as documented in the ``ros2_medkit_fault_manager`` README) must rebuild against the new ``ros2_medkit_msgs`` to keep talking to ``fault_manager``. The in-tree gateway client and server are updated together (`#395 <https://github.com/selfpatch/ros2_medkit/issues/395>`_)
1152
* Per-entity fault routes are now correctly scoped to the entity's hosted apps. ``GET /api/v1/{entity-path}/faults/{fault_code}``, ``DELETE /api/v1/{entity-path}/faults/{fault_code}``, ``GET /api/v1/{entity-path}/faults``, and ``DELETE /api/v1/{entity-path}/faults`` previously fell back to a prefix match against the entity's ``namespace_path``; when that was empty (host-derived / synthetic components, manifest components without a ``namespace`` field, Areas, Functions, and Apps with a wildcard ``ros_binding.namespace_pattern``) the scope filter was silently disabled and the routes exposed - and on ``DELETE``, cleared - faults reported by apps that belonged to entirely different entities. All four handlers now resolve the addressed entity to its hosted-app FQN set (via the new ``HandlerContext::resolve_entity_source_fqns`` helper) and apply a strict all-sources scope check: a fault counts as in scope only when **every** entry in its ``reporting_sources`` is owned by the entity (exact FQN match, or strict path-child via ``<fqn>/...``). Per-fault routes return ``404 Resource Not Found`` for any fault that fails the check; collection routes return an empty ``items`` array. The underlying ``GetFault.srv`` contract is unchanged; ``ClearFault.srv`` gains a new ``skip_correlation_auto_clear`` request flag so per-entity DELETE can opt out of cascade-clearing correlated symptom fault codes that may live in other entities. Per-entity collection responses no longer include the global ``muted_count`` / ``cluster_count`` / ``muted_faults`` / ``clusters`` correlation metadata; those remain on the global ``GET /api/v1/faults`` route. Behavior changes visible to clients: (a) faults reported by apps outside the addressed entity are no longer returned or cleared via that entity's route, (b) **mixed-source** faults that include at least one out-of-entity reporter are likewise rejected with ``404`` on per-fault routes and excluded from per-entity collection responses (use the global ``GET /api/v1/faults`` to see them), (c) per-entity DELETE no longer cascade-clears correlated symptoms outside the entity (`#395 <https://github.com/selfpatch/ros2_medkit/issues/395>`_)
1253
* ``GET /api/v1/updates/{id}/status`` no longer returns ``404`` for a registered-but-idle package; ``POST /api/v1/updates`` now seeds a ``pending`` status, so the endpoint returns ``200 {"status": "pending"}`` immediately after registration. ``404`` is reserved for packages that are not registered. Clients that used ``404`` as a signal for "registered but nothing started yet" must adapt (`#378 <https://github.com/selfpatch/ros2_medkit/issues/378>`_)
1354

1455
**Features:**
1556

57+
* Typed ``fan_out_collection<T>`` aggregating helper replaces raw-JSON ``merge_peer_items`` on the typed collection routes (data, operations, config, logs). Peer items are decoded via ``dto::JsonReader<T>``; items that fail validation are removed from the merged ``items`` array, recorded in ``x-medkit.peer_dropped_items`` with the JsonReader error plus a best-effort ``source_id``, and logged at ``WARN``. Previously, malformed peer items silently disappeared into the merged response; fleet operators can now detect inter-gateway schema drift directly on the wire (`#403 <https://github.com/selfpatch/ros2_medkit/issues/403>`_)
58+
* ``Collection<T, XMedkitT>`` is now a 2-parameter template. Domain list endpoints (faults, config, data, logs) reference their richer per-domain collection x-medkit struct (``FaultListXMedkit``, ``ConfigListXMedkit``, ``DataListXMedkit``, ``LogListXMedkit``) directly in the published schema instead of the generic ``XMedkitCollection``, so generated clients see aggregation counts, peer provenance, and ``peer_dropped_items`` from the schema (`#403 <https://github.com/selfpatch/ros2_medkit/issues/403>`_)
59+
* New ``opaque_object("key", &T::field)`` DTO field descriptor in ``dto/contract.hpp``. Binds a ``nlohmann::json`` member as a typed "any JSON object" field: ``JsonWriter`` emits it verbatim, ``JsonReader`` rejects scalars / arrays / null, ``SchemaWriter`` emits ``{type: object, additionalProperties: true, x-medkit-opaque: true}``. Used for fields whose runtime shape is decided by an upstream component the gateway cannot introspect (live ROS message payloads, plugin-defined fault envelopes, action results) (`#403 <https://github.com/selfpatch/ros2_medkit/issues/403>`_)
1660
* ``GET /api/v1/faults/stream`` event payloads now carry an optional ``x-medkit`` SOVD payload-extension object with ``entity_type`` and ``entity_id`` fields. When the gateway can resolve the fault's first reporting source back to a SOVD entity (via the manifest-mode linking index, or a runtime-mode last-segment match against an existing App), consumers can hit ``/{entity_type}/{entity_id}/bulk-data/rosbags/{fault_code}`` directly instead of HEAD-probing every entity. Resolution is snapshotted at event arrival, so a discovery refresh between enqueue and stream-out cannot retroactively change the entity reported to consumers. The ``x-medkit`` object is omitted entirely when no entity can be resolved, so existing SSE consumers ignore the addition (`#380 <https://github.com/selfpatch/ros2_medkit/issues/380>`_)
1761
* Plugin API version bumped to v7. Adds ``PluginContext::notify_entities_changed(EntityChangeScope)`` lifecycle hook for plugins that mutate the entity surface at runtime; default no-op keeps v6 source code compiling unchanged against v7 headers. Binary compatibility is not provided: the plugin loader uses a strict equality check on ``plugin_api_version()``, so out-of-tree plugins must be recompiled (`#376 <https://github.com/selfpatch/ros2_medkit/issues/376>`_)
1862
* New ``discovery.manifest.fragments_dir`` parameter: gateway scans the directory for ``*.yaml`` / ``*.yml`` fragment files on every manifest load / reload and merges apps, components, and functions on top of the base manifest. Fragments are forbidden from declaring top-level ``areas``, ``metadata``, ``discovery``, ``scripts``, ``capabilities``, or ``lock_overrides`` - those stay in the base manifest. Presence of any forbidden key (including empty-valued ones like ``areas: []``) is reported as a ``FRAGMENT_FORBIDDEN_FIELD`` validation error that fails the load / reload. Unknown top-level keys (typos such as ``app:`` vs ``apps:``) are ignored with a warning log. Files merged in alphabetical order for deterministic duplicate-id errors (`#376 <https://github.com/selfpatch/ros2_medkit/issues/376>`_)

0 commit comments

Comments
 (0)