diff --git a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/discovery/entity_discovery.hpp b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/discovery/entity_discovery.hpp new file mode 100644 index 000000000..93d07715e --- /dev/null +++ b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/discovery/entity_discovery.hpp @@ -0,0 +1,56 @@ +// Copyright 2026 bburda +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "ros2_medkit_gateway/core/discovery/discovery_enums.hpp" +#include "ros2_medkit_gateway/core/discovery/linking_result.hpp" +#include "ros2_medkit_gateway/core/discovery/manifest/manifest.hpp" +#include "ros2_medkit_gateway/core/discovery/merge_types.hpp" +#include "ros2_medkit_gateway/core/discovery/models/app.hpp" +#include "ros2_medkit_gateway/core/discovery/models/area.hpp" +#include "ros2_medkit_gateway/core/discovery/models/component.hpp" +#include "ros2_medkit_gateway/core/discovery/models/function.hpp" + +#include +#include + +namespace ros2_medkit_gateway { + +/// Entity lookups and discovery status the HTTP layer reads when the entity cache has no answer. +class EntityDiscovery { + public: + EntityDiscovery() = default; + virtual ~EntityDiscovery() = default; + EntityDiscovery(const EntityDiscovery &) = delete; + EntityDiscovery & operator=(const EntityDiscovery &) = delete; + EntityDiscovery(EntityDiscovery &&) = delete; + EntityDiscovery & operator=(EntityDiscovery &&) = delete; + + virtual std::optional get_area(const std::string & id) = 0; + virtual std::optional get_component(const std::string & id) = 0; + virtual std::optional get_app(const std::string & id) = 0; + virtual std::optional get_function(const std::string & id) = 0; + + virtual DiscoveryMode get_mode() const = 0; + virtual std::string get_strategy_name() const = 0; + /// Hybrid mode only. + virtual std::optional get_merge_report() const = 0; + /// Hybrid mode only. + virtual std::optional get_linking_result() const = 0; + /// Defaults when no manifest is loaded. + virtual discovery::ManifestConfig get_manifest_config() const = 0; +}; + +} // namespace ros2_medkit_gateway diff --git a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/discovery/linking_result.hpp b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/discovery/linking_result.hpp new file mode 100644 index 000000000..3d2e2b54f --- /dev/null +++ b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/discovery/linking_result.hpp @@ -0,0 +1,76 @@ +// Copyright 2026 bburda +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "ros2_medkit_gateway/core/discovery/manifest/manifest.hpp" +#include "ros2_medkit_gateway/core/discovery/models/app.hpp" + +#include +#include +#include +#include + +namespace ros2_medkit_gateway { +namespace discovery { + +/** + * @brief Result of runtime linking operation + * + * Contains all the information about how manifest Apps were linked + * to runtime ROS 2 nodes, including orphan detection. + */ +struct LinkingResult { + /// Apps with successfully bound ROS nodes (and those that failed) + std::vector linked_apps; + + /// App IDs that have no matching ROS node (offline or external) + std::vector unlinked_app_ids; + + /// ROS node FQNs that have no matching manifest App (orphans) + std::vector orphan_nodes; + + /// Mapping from App ID to bound node FQN + std::unordered_map app_to_node; + + /// Mapping from node FQN to App ID (reverse lookup) + std::unordered_map node_to_app; + + /// Number of times two apps competed for the same node + size_t binding_conflicts{0}; + + /// Number of wildcard bindings that matched >1 node + size_t wildcard_multi_match{0}; + + /// Human-readable diagnostic warnings + std::vector warnings; + + /// Check if linking produced any errors based on policy + bool has_errors(ManifestConfig::UnmanifestedNodePolicy policy) const { + return policy == ManifestConfig::UnmanifestedNodePolicy::ERROR && !orphan_nodes.empty(); + } + + /// Get statistics summary + std::string summary() const { + std::string s = std::to_string(app_to_node.size()) + " linked, " + std::to_string(unlinked_app_ids.size()) + + " unlinked, " + std::to_string(orphan_nodes.size()) + " orphan nodes"; + if (binding_conflicts > 0) { + s += ", " + std::to_string(binding_conflicts) + " binding conflict(s)"; + } + return s; + } +}; + +} // namespace discovery +} // namespace ros2_medkit_gateway diff --git a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/gateway_services.hpp b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/gateway_services.hpp index 64ff2b513..aae937c09 100644 --- a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/gateway_services.hpp +++ b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/gateway_services.hpp @@ -20,7 +20,7 @@ namespace ros2_medkit_gateway { class ThreadSafeEntityCache; class PluginManager; -class DiscoveryManager; +class EntityDiscovery; class ScriptManager; class FaultManager; class LockManager; @@ -55,7 +55,7 @@ class GatewayServices { // Never null. virtual const ThreadSafeEntityCache & entity_cache() const = 0; - virtual DiscoveryManager * discovery() = 0; + virtual EntityDiscovery * discovery() = 0; virtual FaultManager * fault_manager() = 0; virtual ConfigurationManager * configuration_manager() = 0; virtual DataAccessManager * data_access_manager() = 0; diff --git a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/discovery/discovery_manager.hpp b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/discovery/discovery_manager.hpp index b7b99cfe3..bb9129f1a 100644 --- a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/discovery/discovery_manager.hpp +++ b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/discovery/discovery_manager.hpp @@ -15,6 +15,7 @@ #pragma once #include "ros2_medkit_gateway/core/discovery/discovery_enums.hpp" +#include "ros2_medkit_gateway/core/discovery/entity_discovery.hpp" #include "ros2_medkit_gateway/core/discovery/layers/manifest_layer.hpp" #include "ros2_medkit_gateway/core/discovery/merge_types.hpp" #include "ros2_medkit_gateway/core/discovery/models/app.hpp" @@ -144,7 +145,7 @@ struct DiscoveryConfig { * @see discovery::MergePipeline * @see discovery::ManifestManager */ -class DiscoveryManager : public ServiceActionResolver { +class DiscoveryManager : public ServiceActionResolver, public EntityDiscovery { public: /** * @brief Construct the discovery manager @@ -226,28 +227,28 @@ class DiscoveryManager : public ServiceActionResolver { * @param id Area identifier * @return Area if found */ - std::optional get_area(const std::string & id); + std::optional get_area(const std::string & id) override; /** * @brief Get component by ID * @param id Component identifier * @return Component if found */ - std::optional get_component(const std::string & id); + std::optional get_component(const std::string & id) override; /** * @brief Get app by ID * @param id App identifier * @return App if found */ - std::optional get_app(const std::string & id); + std::optional get_app(const std::string & id) override; /** * @brief Get function by ID * @param id Function identifier * @return Function if found */ - std::optional get_function(const std::string & id); + std::optional get_function(const std::string & id) override; // ========================================================================= // Relationship queries @@ -390,7 +391,7 @@ class DiscoveryManager : public ServiceActionResolver { * default-constructed ManifestConfig in those cases - the same values the * pipeline itself runs with. */ - discovery::ManifestConfig get_manifest_config() const; + discovery::ManifestConfig get_manifest_config() const override; /** * @brief Re-read the manifest configuration into the running pipeline @@ -424,7 +425,7 @@ class DiscoveryManager : public ServiceActionResolver { * @brief Get current discovery mode * @return Active discovery mode */ - DiscoveryMode get_mode() const { + DiscoveryMode get_mode() const override { return config_.mode; } @@ -432,19 +433,19 @@ class DiscoveryManager : public ServiceActionResolver { * @brief Get a human-readable name for the active discovery configuration. * @return One of "runtime", "manifest", "hybrid". */ - std::string get_strategy_name() const; + std::string get_strategy_name() const override; /** * @brief Get the last merge pipeline report (hybrid mode only) * @return MergeReport if in hybrid mode, nullopt otherwise */ - std::optional get_merge_report() const; + std::optional get_merge_report() const override; /** * @brief Get the last linking result (hybrid mode only) * @return LinkingResult if in hybrid mode, nullopt otherwise */ - std::optional get_linking_result() const; + std::optional get_linking_result() const override; private: /** diff --git a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/discovery/manifest/runtime_linker.hpp b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/discovery/manifest/runtime_linker.hpp index 6a14fa4d9..1264d66ef 100644 --- a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/discovery/manifest/runtime_linker.hpp +++ b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/discovery/manifest/runtime_linker.hpp @@ -14,6 +14,7 @@ #pragma once +#include "ros2_medkit_gateway/core/discovery/linking_result.hpp" #include "ros2_medkit_gateway/core/discovery/manifest/manifest.hpp" #include "ros2_medkit_gateway/core/discovery/models/app.hpp" @@ -29,53 +30,6 @@ namespace ros2_medkit_gateway { namespace discovery { -/** - * @brief Result of runtime linking operation - * - * Contains all the information about how manifest Apps were linked - * to runtime ROS 2 nodes, including orphan detection. - */ -struct LinkingResult { - /// Apps with successfully bound ROS nodes (and those that failed) - std::vector linked_apps; - - /// App IDs that have no matching ROS node (offline or external) - std::vector unlinked_app_ids; - - /// ROS node FQNs that have no matching manifest App (orphans) - std::vector orphan_nodes; - - /// Mapping from App ID to bound node FQN - std::unordered_map app_to_node; - - /// Mapping from node FQN to App ID (reverse lookup) - std::unordered_map node_to_app; - - /// Number of times two apps competed for the same node - size_t binding_conflicts{0}; - - /// Number of wildcard bindings that matched >1 node - size_t wildcard_multi_match{0}; - - /// Human-readable diagnostic warnings - std::vector warnings; - - /// Check if linking produced any errors based on policy - bool has_errors(ManifestConfig::UnmanifestedNodePolicy policy) const { - return policy == ManifestConfig::UnmanifestedNodePolicy::ERROR && !orphan_nodes.empty(); - } - - /// Get statistics summary - std::string summary() const { - std::string s = std::to_string(app_to_node.size()) + " linked, " + std::to_string(unlinked_app_ids.size()) + - " unlinked, " + std::to_string(orphan_nodes.size()) + " orphan nodes"; - if (binding_conflicts > 0) { - s += ", " + std::to_string(binding_conflicts) + " binding conflict(s)"; - } - return s; - } -}; - /** * @brief Links manifest Apps to runtime ROS 2 nodes * diff --git a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/gateway_node.hpp b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/gateway_node.hpp index c6240adc3..8810e5a18 100644 --- a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/gateway_node.hpp +++ b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/gateway_node.hpp @@ -97,7 +97,7 @@ class GatewayNode : public rclcpp::Node, public GatewayServices { PluginManager * plugin_manager() override { return get_plugin_manager(); } - DiscoveryManager * discovery() override { + EntityDiscovery * discovery() override { return get_discovery_manager(); } ScriptManager * script_manager() override { diff --git a/src/ros2_medkit_gateway/scripts/check_core_purity.sh b/src/ros2_medkit_gateway/scripts/check_core_purity.sh index d7bd2884e..f7349c799 100755 --- a/src/ros2_medkit_gateway/scripts/check_core_purity.sh +++ b/src/ros2_medkit_gateway/scripts/check_core_purity.sh @@ -51,6 +51,8 @@ done # Files that must compile without any ROS include path. ROS_FREE_SOURCES=( "include/ros2_medkit_gateway/core/gateway_services.hpp" + "include/ros2_medkit_gateway/core/discovery/entity_discovery.hpp" + "include/ros2_medkit_gateway/core/discovery/linking_result.hpp" "include/ros2_medkit_gateway/http/handlers/handler_context.hpp" "src/http/handlers/handler_context.cpp" ) diff --git a/src/ros2_medkit_gateway/src/http/handlers/discovery_handlers.cpp b/src/ros2_medkit_gateway/src/http/handlers/discovery_handlers.cpp index 5e8e3580f..cd27f8160 100644 --- a/src/ros2_medkit_gateway/src/http/handlers/discovery_handlers.cpp +++ b/src/ros2_medkit_gateway/src/http/handlers/discovery_handlers.cpp @@ -20,6 +20,7 @@ #include #include +#include "ros2_medkit_gateway/core/discovery/entity_discovery.hpp" #include "ros2_medkit_gateway/core/discovery/models/common.hpp" #include "ros2_medkit_gateway/core/http/error_codes.hpp" #include "ros2_medkit_gateway/core/http/handlers/capability_builder.hpp" @@ -28,7 +29,6 @@ #include "ros2_medkit_gateway/core/managers/script_manager.hpp" #include "ros2_medkit_gateway/core/models/thread_safe_entity_cache.hpp" #include "ros2_medkit_gateway/core/plugins/plugin_manager.hpp" -#include "ros2_medkit_gateway/discovery/discovery_manager.hpp" #include "ros2_medkit_gateway/dto/entities.hpp" #include "ros2_medkit_gateway/http/handlers/handler_support.hpp" diff --git a/src/ros2_medkit_gateway/src/http/handlers/health_handlers.cpp b/src/ros2_medkit_gateway/src/http/handlers/health_handlers.cpp index 0e91d5247..fcdebc62f 100644 --- a/src/ros2_medkit_gateway/src/http/handlers/health_handlers.cpp +++ b/src/ros2_medkit_gateway/src/http/handlers/health_handlers.cpp @@ -21,6 +21,7 @@ #include "ros2_medkit_gateway/core/auth/auth_models.hpp" #include "ros2_medkit_gateway/core/data/topic_data_provider.hpp" #include "ros2_medkit_gateway/core/discovery/discovery_enums.hpp" +#include "ros2_medkit_gateway/core/discovery/entity_discovery.hpp" #include "ros2_medkit_gateway/core/entity_validation.hpp" #include "ros2_medkit_gateway/core/http/error_codes.hpp" #include "ros2_medkit_gateway/core/http/fan_out_helpers.hpp" @@ -33,7 +34,6 @@ #include "ros2_medkit_gateway/core/models/thread_safe_entity_cache.hpp" #include "ros2_medkit_gateway/core/plugins/plugin_manager.hpp" #include "ros2_medkit_gateway/core/version.hpp" -#include "ros2_medkit_gateway/discovery/discovery_manager.hpp" #include "ros2_medkit_gateway/dto/health.hpp" #include "../../openapi/route_registry.hpp" diff --git a/src/ros2_medkit_gateway/test/null_gateway_services.hpp b/src/ros2_medkit_gateway/test/null_gateway_services.hpp index b46ddcc26..0ddf24e40 100644 --- a/src/ros2_medkit_gateway/test/null_gateway_services.hpp +++ b/src/ros2_medkit_gateway/test/null_gateway_services.hpp @@ -32,7 +32,7 @@ class NullGatewayServices final : public GatewayServices { PluginManager * plugin_manager() override { return nullptr; } - DiscoveryManager * discovery() override { + EntityDiscovery * discovery() override { return nullptr; } ScriptManager * script_manager() override {