Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 <optional>
#include <string>

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<Area> get_area(const std::string & id) = 0;
virtual std::optional<Component> get_component(const std::string & id) = 0;
virtual std::optional<App> get_app(const std::string & id) = 0;
virtual std::optional<Function> 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<discovery::MergeReport> get_merge_report() const = 0;
/// Hybrid mode only.
virtual std::optional<discovery::LinkingResult> get_linking_result() const = 0;
/// Defaults when no manifest is loaded.
virtual discovery::ManifestConfig get_manifest_config() const = 0;
};

} // namespace ros2_medkit_gateway
Original file line number Diff line number Diff line change
@@ -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 <cstddef>
#include <string>
#include <unordered_map>
#include <vector>

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<App> linked_apps;

/// App IDs that have no matching ROS node (offline or external)
std::vector<std::string> unlinked_app_ids;

/// ROS node FQNs that have no matching manifest App (orphans)
std::vector<std::string> orphan_nodes;

/// Mapping from App ID to bound node FQN
std::unordered_map<std::string, std::string> app_to_node;

/// Mapping from node FQN to App ID (reverse lookup)
std::unordered_map<std::string, std::string> 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<std::string> 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
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ namespace ros2_medkit_gateway {

class ThreadSafeEntityCache;
class PluginManager;
class DiscoveryManager;
class EntityDiscovery;
class ScriptManager;
class FaultManager;
class LockManager;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -226,28 +227,28 @@ class DiscoveryManager : public ServiceActionResolver {
* @param id Area identifier
* @return Area if found
*/
std::optional<Area> get_area(const std::string & id);
std::optional<Area> get_area(const std::string & id) override;

/**
* @brief Get component by ID
* @param id Component identifier
* @return Component if found
*/
std::optional<Component> get_component(const std::string & id);
std::optional<Component> get_component(const std::string & id) override;

/**
* @brief Get app by ID
* @param id App identifier
* @return App if found
*/
std::optional<App> get_app(const std::string & id);
std::optional<App> get_app(const std::string & id) override;

/**
* @brief Get function by ID
* @param id Function identifier
* @return Function if found
*/
std::optional<Function> get_function(const std::string & id);
std::optional<Function> get_function(const std::string & id) override;

// =========================================================================
// Relationship queries
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -424,27 +425,27 @@ 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;
}

/**
* @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<discovery::MergeReport> get_merge_report() const;
std::optional<discovery::MergeReport> get_merge_report() const override;

/**
* @brief Get the last linking result (hybrid mode only)
* @return LinkingResult if in hybrid mode, nullopt otherwise
*/
std::optional<discovery::LinkingResult> get_linking_result() const;
std::optional<discovery::LinkingResult> get_linking_result() const override;

private:
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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<App> linked_apps;

/// App IDs that have no matching ROS node (offline or external)
std::vector<std::string> unlinked_app_ids;

/// ROS node FQNs that have no matching manifest App (orphans)
std::vector<std::string> orphan_nodes;

/// Mapping from App ID to bound node FQN
std::unordered_map<std::string, std::string> app_to_node;

/// Mapping from node FQN to App ID (reverse lookup)
std::unordered_map<std::string, std::string> 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<std::string> 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
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions src/ros2_medkit_gateway/scripts/check_core_purity.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include <set>
#include <variant>

#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"
Expand All @@ -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"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/ros2_medkit_gateway/test/null_gateway_services.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading