Skip to content

refactor(gateway): introduce core/ layer scaffolding for provider injection#392

Draft
bburda wants to merge 20 commits intomainfrom
feat/medkit-core-scaffold
Draft

refactor(gateway): introduce core/ layer scaffolding for provider injection#392
bburda wants to merge 20 commits intomainfrom
feat/medkit-core-scaffold

Conversation

@bburda
Copy link
Copy Markdown
Collaborator

@bburda bburda commented Apr 27, 2026

Pull Request

Summary

Splits src/ros2_medkit_gateway/ into a middleware-neutral core/ layer and the existing top-level layout (ROS adapters), introducing two named build targets and two structural CI guards as outlined in the linked issue. Pure structural change — no business logic edits, no signature changes, no public API changes for consumers. Subsequent issues will cover the per-manager refactors (data access, operation, configuration, fault facade, log, trigger) that introduce provider injection so the managers themselves move into core/.

What changed:

  • 35 source files relocated into src/core/ (include/ros2_medkit_gateway/core/ for headers). New gateway_core STATIC library compiles them with header-only and C-level externals (cpp-httplib, nlohmann/json, yaml-cpp, tl::expected, jwt-cpp, OpenSSL, SQLite, dl) and zero ROS dependencies.
  • The remaining 56 ROS-coupled sources stay at their current paths and form a new gateway_ros2 STATIC library that publicly links gateway_core. gateway_node and the existing test targets link gateway_ros2, so they transitively get both layers.
  • Every #include path inside the package rewritten via sed against the rename history of the relocation commits — pure mechanical edit, no other source changes.
  • compat.hpp introduces namespace ros2_medkit_gateway = medkit; so call sites that still reference the legacy namespace continue to compile during the gradual migration.
  • Backwards-compat shim headers at every legacy path (providers/, plugins/, discovery/models/, http/error_codes.hpp, discovery/introspection_provider.hpp) so out-of-tree consumers (graph_provider, opcua, sovd_service_interface, linux_introspection, param_beacon, topic_beacon) compile without modification.
  • scripts/check_core_purity.sh greps core/ for any direct ROS include and is wired as the gateway_core_purity CTest under the linter label. Smoke-tested by planting a #include <rclcpp/rclcpp.hpp> — correctly rejected.
  • test_gateway_core_smoke compiles a translation unit that includes a sampling of core/ headers and links exclusively against gateway_core + GTest, with no ament_target_dependencies. Catches transitive ROS coupling that the grep guard might miss.
  • PCH split: gateway_core PCH covers std + nlohmann/json.hpp + httplib.h + tl/expected.hpp; gateway_ros2 PCH adds rclcpp/rclcpp.hpp on top.
  • Pre-emptive clang-tidy fixes (cppcoreguidelines-special-member-functions Rule of Five, bugprone-exception-escape on main, performance-unnecessary-value-param) surfaced by the new build setup.
  • Bonus: removes a timing race in test_combined_introspection.test_01_procfs_returns_200_on_host where a freshly forked process could momentarily report RssAnon=0 before any memory was touched. The poll helper now waits for rss_bytes > 0 before asserting.

Issue


Type

  • Bug fix
  • New feature or tests
  • Breaking change
  • Documentation only

Testing

Local validation on Jazzy:

Stage Result
colcon build --symlink-install (15 packages) clean (~4 min)
./scripts/test.sh unit 2561 tests, 0 errors, 0 failures
./scripts/test.sh lint --packages-select ros2_medkit_gateway 3494 tests, 0 errors, 0 failures (includes the new gateway_core_purity linter)
./scripts/test.sh integ --packages-select ros2_medkit_integration_tests 3211 tests, 0 errors, 0 failures
./scripts/test.sh tidy --packages-select ros2_medkit_gateway 2636 tests, 0 errors, 0 failures (clang-tidy run, ~32 min)
colcon test --ctest-args -R gateway_core_smoke passes — compiles and links against gateway_core alone with no ament_target_dependencies
gateway_core_purity planted-violation smoke test a planted #include <rclcpp/rclcpp.hpp> in core/ was correctly rejected

Reviewers should verify: (1) gateway_core_purity runs as part of the linter job, (2) test_gateway_core_smoke builds and runs without ament dependencies, (3) downstream packages that consume the gateway's headers still compile via the shim layer.


Checklist

  • Breaking changes are clearly described (and announced in docs / changelog if needed) — none, public API and namespaces preserved via compat.hpp alias and shim headers
  • Tests were added or updated if needed — gateway_core_purity linter, test_gateway_core_smoke link test, and a polling fix in test_combined_introspection
  • Docs were updated if behavior or public API changed — no public API change; CHANGELOG entries are added at release time per project convention

bburda added 20 commits April 27, 2026 18:26
Move provider abstract interfaces (DataProvider, OperationProvider,
FaultProvider, LogProvider, ScriptProvider, UpdateProvider,
IntrospectionProvider, HostInfoProvider) to the middleware-neutral core
layer. No logic, no signature changes. Provider concrete implementations
will be relocated alongside their respective manager refactors.
REST server router, rate limiter, SSE client tracker, HTTP server,
HTTP utilities, x-medkit vendor extensions, error codes, warning codes,
entity path utilities, and fan-out helpers carry no transport-layer ROS
dependencies and belong to the middleware-neutral core.
…andlers/

Handlers verified middleware-neutral by includes audit. Handlers with
transitive logger or subscription dependencies (handler_context,
fault_handlers, sse_fault_handler, cyclic_subscription_handlers) remain
at their current paths and will relocate alongside the corresponding
manager refactors.
JWT authentication, role-based authorization, middleware policy, and
auth model types form a self-contained, middleware-neutral subsystem.
…aggregation/

Peer client, entity merger, mDNS discovery, network utilities, SSE
stream proxy, and classification utilities are HTTP-only and
middleware-neutral. The aggregation_manager orchestrator currently
couples to the gateway node via rclcpp::Logger and remains in place
for a follow-up refactor.
…/models/

Area, Component, App, and Function entity types - the SOVD entity model
shared across all discovery layers - are pure data structures with no
middleware dependencies.
Lock manager, bulk data store, subscription manager, script manager,
update manager, trigger manager, entity cache, and related transport
and type headers are middleware-neutral and form the core business-logic
layer. Plugin framework, OpenAPI, data types, discovery enums, strategy
interfaces, manifest parser, and manifest/layer headers are also neutral
and are co-located under their respective core/ subdirectories. Managers
that today take rclcpp::Node* (data access, operation, configuration,
fault facade, log, trigger subscriber) remain in place pending their
provider injection refactors.
Every source file whose header was relocated under core/ in earlier
commits and which itself carries no ROS includes joins its header
under the matching src/core/ path. A handful of source files remain
in place because they contain ROS-specific code that will move only
once the corresponding manager refactor lands (rest_server, http_server,
sse_transport_provider, trigger_handlers, default_script_provider,
script_manager, plugin_loader, plugin_manager, plugin_context).
Introduces the medkit:: neutral namespace and an alias mapping the
legacy ros2_medkit_gateway:: namespace onto it. Allows call sites that
still reference the legacy namespace to compile while individual files
migrate to the new namespace one subsystem at a time.
Updates every #include directive in the package to reflect the relocated
header paths under ros2_medkit_gateway/core/. Pure mechanical rewrite —
no source-content edits beyond include statements. Generated by sed
substitution against the rename history of the preceding eight commits.
gateway_core is a position-independent static library compiled from
sources under src/core/ via GLOB_RECURSE; it links only header-only
and C-level externals (cpp-httplib, nlohmann/json, yaml-cpp,
tl::expected, jwt-cpp, OpenSSL, SQLite, dl) and carries no rclcpp
dependency.

gateway_ros2 is the ROS adapter static library compiled from the
remaining sources under src/. It depends on rclcpp / rcl_interfaces /
action_msgs / ros2_medkit_msgs / ros2_medkit_serialization /
rosidl_typesupport via medkit_target_dependencies and links gateway_core
so the executable and tests get both layers from a single dependency.

Source audit during build iteration moved 22 files from src/core/ back
to src/ because their include chains transitively pull in rclcpp or
ros2_medkit_serialization: all HTTP handler implementations (via
handler_context.hpp -> rclcpp), openapi spec/schema builders (via
schema_builder.hpp -> ros2_medkit_serialization), type_introspection,
manifest_parser, manifest_layer, runtime_layer, trigger_manager, and
plugin_http_types. path_resolver.cpp and route_registry.cpp remain in
gateway_core as they have no ROS dependencies.

The gateway_node executable now links gateway_ros2; all test targets
follow. PCH split between layers: gateway_core's PCH contains std +
nlohmann/json + httplib + tl::expected; gateway_ros2 extends with
rclcpp/rclcpp.hpp. Private openapi headers in src/openapi/ are exposed
to both targets via PRIVATE target_include_directories.

All 81 unit test targets pass (2098 assertions).
Greps include/ros2_medkit_gateway/core/ and src/core/ for any direct
include of ROS-specific headers (rclcpp, rcl_interfaces, rosidl,
action_msgs, message-package families, ros2_medkit_msgs, rcutils, rmw).
Exits non-zero on any match. Wired into CTest as a linter target by
the next commit so the build fails when the neutral layer contract is
violated.
Adds gateway_core_purity to the linter-labeled CTest suite so it runs
in the linter CI job alongside ament_lint_cmake, clang-format, and
the other static checks. Build fails on any direct ROS include leaked
into the neutral core layer.
Compiles a translation unit including a sampling of core/ public headers
(entity model types, provider interface contracts) and links exclusively
against gateway_core + GTest. No ament_target_dependencies, no rclcpp
on the link line. A ROS transitive include silently reaching the core
layer surfaces here as a missing-symbol link error, catching leaks
that the grep-based purity script can miss when the include is reached
through a third-party header.
Adds static_asserts and namespace using-declarations for each included
entity model (Area, Component, App, Function) and provider interface
(DataProvider, OperationProvider, FaultProvider, LogProvider,
IntrospectionProvider) so the includes are recognized as used by
include-cleaner tooling. The link-time invariant still rests on linking
gateway_core alone with no ament_target_dependencies.
Plugins consuming the gateway's public API still reference the pre-scaffold
include paths (plugins/, providers/, discovery/models/, http/error_codes.hpp,
discovery/introspection_provider.hpp). Add forwarding shims at the old paths
so downstream packages (graph_provider, opcua, sovd_service_interface, linux
introspection, param_beacon, topic_beacon) compile without change.
Add missing Rule of Five = delete declarations to classes with
user-defined destructors: SSEStreamProxy, ConfigurationManager,
DefaultScriptProvider, Ros2TopicDataProvider, RESTServer, LogManager,
OperationManager. Add move = delete to SqliteStatement (already had
copy = delete). Fix NotifyGuard anonymous struct to satisfy the rule.
Fix performance-unnecessary-value-param in Ros2TopicDataProvider
subscription callback. Wrap main() body in outer try/catch to
prevent exception escape (bugprone-exception-escape).
Removes a timing race in TestCombinedIntrospection.test_01_procfs_returns_200_on_host
where a fresh process can momentarily report RssAnon=0 in /proc/<pid>/status
before any memory has been touched. The poll helper now waits for both a 200
response AND rss_bytes > 0 before returning, eliminating intermittent
'0 not greater than 0' failures observed in local runs.
readability-named-parameter: name the unused override parameters in the
mock plugin classes via comment placeholders so the compiler-visible
parameter list satisfies the rule without changing semantics.

performance-inefficient-vector-operation: reserve the std::thread
vector capacity in the three concurrency stress tests before the
emplace_back loops so no incremental reallocation happens during the
hot path.

performance-for-range-copy: take the test parameter pair by const
reference instead of by value in CyclicSubscriptionJsonTest.
The neutral entity model imports nlohmann/json under the alias
ros2_medkit_gateway::json (defined in core/auth/auth_models.hpp). Two
test files re-declared a local using-alias of the same name, which
clang-diagnostic-shadow flags as an error once the test pulls in any
core/ header that exposes the namespace alias. Drop the local alias
and rely on the namespace import.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Split gateway sources into a middleware-neutral core layer for independent testing and structural enforcement

1 participant