Skip to content

Commit 281ff30

Browse files
authored
refactor(gateway): reach discovery through an EntityDiscovery interface
Handlers and the entity cache read entities through a ROS-free interface. The ROS discovery manager implements it.
1 parent 7e0d875 commit 281ff30

10 files changed

Lines changed: 152 additions & 63 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright 2026 bburda
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#pragma once
16+
17+
#include "ros2_medkit_gateway/core/discovery/discovery_enums.hpp"
18+
#include "ros2_medkit_gateway/core/discovery/linking_result.hpp"
19+
#include "ros2_medkit_gateway/core/discovery/manifest/manifest.hpp"
20+
#include "ros2_medkit_gateway/core/discovery/merge_types.hpp"
21+
#include "ros2_medkit_gateway/core/discovery/models/app.hpp"
22+
#include "ros2_medkit_gateway/core/discovery/models/area.hpp"
23+
#include "ros2_medkit_gateway/core/discovery/models/component.hpp"
24+
#include "ros2_medkit_gateway/core/discovery/models/function.hpp"
25+
26+
#include <optional>
27+
#include <string>
28+
29+
namespace ros2_medkit_gateway {
30+
31+
/// Entity lookups and discovery status the HTTP layer reads when the entity cache has no answer.
32+
class EntityDiscovery {
33+
public:
34+
EntityDiscovery() = default;
35+
virtual ~EntityDiscovery() = default;
36+
EntityDiscovery(const EntityDiscovery &) = delete;
37+
EntityDiscovery & operator=(const EntityDiscovery &) = delete;
38+
EntityDiscovery(EntityDiscovery &&) = delete;
39+
EntityDiscovery & operator=(EntityDiscovery &&) = delete;
40+
41+
virtual std::optional<Area> get_area(const std::string & id) = 0;
42+
virtual std::optional<Component> get_component(const std::string & id) = 0;
43+
virtual std::optional<App> get_app(const std::string & id) = 0;
44+
virtual std::optional<Function> get_function(const std::string & id) = 0;
45+
46+
virtual DiscoveryMode get_mode() const = 0;
47+
virtual std::string get_strategy_name() const = 0;
48+
/// Hybrid mode only.
49+
virtual std::optional<discovery::MergeReport> get_merge_report() const = 0;
50+
/// Hybrid mode only.
51+
virtual std::optional<discovery::LinkingResult> get_linking_result() const = 0;
52+
/// Defaults when no manifest is loaded.
53+
virtual discovery::ManifestConfig get_manifest_config() const = 0;
54+
};
55+
56+
} // namespace ros2_medkit_gateway
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright 2026 bburda
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#pragma once
16+
17+
#include "ros2_medkit_gateway/core/discovery/manifest/manifest.hpp"
18+
#include "ros2_medkit_gateway/core/discovery/models/app.hpp"
19+
20+
#include <cstddef>
21+
#include <string>
22+
#include <unordered_map>
23+
#include <vector>
24+
25+
namespace ros2_medkit_gateway {
26+
namespace discovery {
27+
28+
/**
29+
* @brief Result of runtime linking operation
30+
*
31+
* Contains all the information about how manifest Apps were linked
32+
* to runtime ROS 2 nodes, including orphan detection.
33+
*/
34+
struct LinkingResult {
35+
/// Apps with successfully bound ROS nodes (and those that failed)
36+
std::vector<App> linked_apps;
37+
38+
/// App IDs that have no matching ROS node (offline or external)
39+
std::vector<std::string> unlinked_app_ids;
40+
41+
/// ROS node FQNs that have no matching manifest App (orphans)
42+
std::vector<std::string> orphan_nodes;
43+
44+
/// Mapping from App ID to bound node FQN
45+
std::unordered_map<std::string, std::string> app_to_node;
46+
47+
/// Mapping from node FQN to App ID (reverse lookup)
48+
std::unordered_map<std::string, std::string> node_to_app;
49+
50+
/// Number of times two apps competed for the same node
51+
size_t binding_conflicts{0};
52+
53+
/// Number of wildcard bindings that matched >1 node
54+
size_t wildcard_multi_match{0};
55+
56+
/// Human-readable diagnostic warnings
57+
std::vector<std::string> warnings;
58+
59+
/// Check if linking produced any errors based on policy
60+
bool has_errors(ManifestConfig::UnmanifestedNodePolicy policy) const {
61+
return policy == ManifestConfig::UnmanifestedNodePolicy::ERROR && !orphan_nodes.empty();
62+
}
63+
64+
/// Get statistics summary
65+
std::string summary() const {
66+
std::string s = std::to_string(app_to_node.size()) + " linked, " + std::to_string(unlinked_app_ids.size()) +
67+
" unlinked, " + std::to_string(orphan_nodes.size()) + " orphan nodes";
68+
if (binding_conflicts > 0) {
69+
s += ", " + std::to_string(binding_conflicts) + " binding conflict(s)";
70+
}
71+
return s;
72+
}
73+
};
74+
75+
} // namespace discovery
76+
} // namespace ros2_medkit_gateway

‎src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/gateway_services.hpp‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ namespace ros2_medkit_gateway {
2020

2121
class ThreadSafeEntityCache;
2222
class PluginManager;
23-
class DiscoveryManager;
23+
class EntityDiscovery;
2424
class ScriptManager;
2525
class FaultManager;
2626
class LockManager;
@@ -55,7 +55,7 @@ class GatewayServices {
5555

5656
// Never null.
5757
virtual const ThreadSafeEntityCache & entity_cache() const = 0;
58-
virtual DiscoveryManager * discovery() = 0;
58+
virtual EntityDiscovery * discovery() = 0;
5959
virtual FaultManager * fault_manager() = 0;
6060
virtual ConfigurationManager * configuration_manager() = 0;
6161
virtual DataAccessManager * data_access_manager() = 0;

‎src/ros2_medkit_gateway/include/ros2_medkit_gateway/discovery/discovery_manager.hpp‎

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#pragma once
1616

1717
#include "ros2_medkit_gateway/core/discovery/discovery_enums.hpp"
18+
#include "ros2_medkit_gateway/core/discovery/entity_discovery.hpp"
1819
#include "ros2_medkit_gateway/core/discovery/layers/manifest_layer.hpp"
1920
#include "ros2_medkit_gateway/core/discovery/merge_types.hpp"
2021
#include "ros2_medkit_gateway/core/discovery/models/app.hpp"
@@ -144,7 +145,7 @@ struct DiscoveryConfig {
144145
* @see discovery::MergePipeline
145146
* @see discovery::ManifestManager
146147
*/
147-
class DiscoveryManager : public ServiceActionResolver {
148+
class DiscoveryManager : public ServiceActionResolver, public EntityDiscovery {
148149
public:
149150
/**
150151
* @brief Construct the discovery manager
@@ -226,28 +227,28 @@ class DiscoveryManager : public ServiceActionResolver {
226227
* @param id Area identifier
227228
* @return Area if found
228229
*/
229-
std::optional<Area> get_area(const std::string & id);
230+
std::optional<Area> get_area(const std::string & id) override;
230231

231232
/**
232233
* @brief Get component by ID
233234
* @param id Component identifier
234235
* @return Component if found
235236
*/
236-
std::optional<Component> get_component(const std::string & id);
237+
std::optional<Component> get_component(const std::string & id) override;
237238

238239
/**
239240
* @brief Get app by ID
240241
* @param id App identifier
241242
* @return App if found
242243
*/
243-
std::optional<App> get_app(const std::string & id);
244+
std::optional<App> get_app(const std::string & id) override;
244245

245246
/**
246247
* @brief Get function by ID
247248
* @param id Function identifier
248249
* @return Function if found
249250
*/
250-
std::optional<Function> get_function(const std::string & id);
251+
std::optional<Function> get_function(const std::string & id) override;
251252

252253
// =========================================================================
253254
// Relationship queries
@@ -390,7 +391,7 @@ class DiscoveryManager : public ServiceActionResolver {
390391
* default-constructed ManifestConfig in those cases - the same values the
391392
* pipeline itself runs with.
392393
*/
393-
discovery::ManifestConfig get_manifest_config() const;
394+
discovery::ManifestConfig get_manifest_config() const override;
394395

395396
/**
396397
* @brief Re-read the manifest configuration into the running pipeline
@@ -424,27 +425,27 @@ class DiscoveryManager : public ServiceActionResolver {
424425
* @brief Get current discovery mode
425426
* @return Active discovery mode
426427
*/
427-
DiscoveryMode get_mode() const {
428+
DiscoveryMode get_mode() const override {
428429
return config_.mode;
429430
}
430431

431432
/**
432433
* @brief Get a human-readable name for the active discovery configuration.
433434
* @return One of "runtime", "manifest", "hybrid".
434435
*/
435-
std::string get_strategy_name() const;
436+
std::string get_strategy_name() const override;
436437

437438
/**
438439
* @brief Get the last merge pipeline report (hybrid mode only)
439440
* @return MergeReport if in hybrid mode, nullopt otherwise
440441
*/
441-
std::optional<discovery::MergeReport> get_merge_report() const;
442+
std::optional<discovery::MergeReport> get_merge_report() const override;
442443

443444
/**
444445
* @brief Get the last linking result (hybrid mode only)
445446
* @return LinkingResult if in hybrid mode, nullopt otherwise
446447
*/
447-
std::optional<discovery::LinkingResult> get_linking_result() const;
448+
std::optional<discovery::LinkingResult> get_linking_result() const override;
448449

449450
private:
450451
/**

‎src/ros2_medkit_gateway/include/ros2_medkit_gateway/discovery/manifest/runtime_linker.hpp‎

Lines changed: 1 addition & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
#pragma once
1616

17+
#include "ros2_medkit_gateway/core/discovery/linking_result.hpp"
1718
#include "ros2_medkit_gateway/core/discovery/manifest/manifest.hpp"
1819
#include "ros2_medkit_gateway/core/discovery/models/app.hpp"
1920

@@ -29,53 +30,6 @@
2930
namespace ros2_medkit_gateway {
3031
namespace discovery {
3132

32-
/**
33-
* @brief Result of runtime linking operation
34-
*
35-
* Contains all the information about how manifest Apps were linked
36-
* to runtime ROS 2 nodes, including orphan detection.
37-
*/
38-
struct LinkingResult {
39-
/// Apps with successfully bound ROS nodes (and those that failed)
40-
std::vector<App> linked_apps;
41-
42-
/// App IDs that have no matching ROS node (offline or external)
43-
std::vector<std::string> unlinked_app_ids;
44-
45-
/// ROS node FQNs that have no matching manifest App (orphans)
46-
std::vector<std::string> orphan_nodes;
47-
48-
/// Mapping from App ID to bound node FQN
49-
std::unordered_map<std::string, std::string> app_to_node;
50-
51-
/// Mapping from node FQN to App ID (reverse lookup)
52-
std::unordered_map<std::string, std::string> node_to_app;
53-
54-
/// Number of times two apps competed for the same node
55-
size_t binding_conflicts{0};
56-
57-
/// Number of wildcard bindings that matched >1 node
58-
size_t wildcard_multi_match{0};
59-
60-
/// Human-readable diagnostic warnings
61-
std::vector<std::string> warnings;
62-
63-
/// Check if linking produced any errors based on policy
64-
bool has_errors(ManifestConfig::UnmanifestedNodePolicy policy) const {
65-
return policy == ManifestConfig::UnmanifestedNodePolicy::ERROR && !orphan_nodes.empty();
66-
}
67-
68-
/// Get statistics summary
69-
std::string summary() const {
70-
std::string s = std::to_string(app_to_node.size()) + " linked, " + std::to_string(unlinked_app_ids.size()) +
71-
" unlinked, " + std::to_string(orphan_nodes.size()) + " orphan nodes";
72-
if (binding_conflicts > 0) {
73-
s += ", " + std::to_string(binding_conflicts) + " binding conflict(s)";
74-
}
75-
return s;
76-
}
77-
};
78-
7933
/**
8034
* @brief Links manifest Apps to runtime ROS 2 nodes
8135
*

‎src/ros2_medkit_gateway/include/ros2_medkit_gateway/gateway_node.hpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ class GatewayNode : public rclcpp::Node, public GatewayServices {
9797
PluginManager * plugin_manager() override {
9898
return get_plugin_manager();
9999
}
100-
DiscoveryManager * discovery() override {
100+
EntityDiscovery * discovery() override {
101101
return get_discovery_manager();
102102
}
103103
ScriptManager * script_manager() override {

‎src/ros2_medkit_gateway/scripts/check_core_purity.sh‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ done
5151
# Files that must compile without any ROS include path.
5252
ROS_FREE_SOURCES=(
5353
"include/ros2_medkit_gateway/core/gateway_services.hpp"
54+
"include/ros2_medkit_gateway/core/discovery/entity_discovery.hpp"
55+
"include/ros2_medkit_gateway/core/discovery/linking_result.hpp"
5456
"include/ros2_medkit_gateway/http/handlers/handler_context.hpp"
5557
"src/http/handlers/handler_context.cpp"
5658
)

‎src/ros2_medkit_gateway/src/http/handlers/discovery_handlers.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include <set>
2121
#include <variant>
2222

23+
#include "ros2_medkit_gateway/core/discovery/entity_discovery.hpp"
2324
#include "ros2_medkit_gateway/core/discovery/models/common.hpp"
2425
#include "ros2_medkit_gateway/core/http/error_codes.hpp"
2526
#include "ros2_medkit_gateway/core/http/handlers/capability_builder.hpp"
@@ -28,7 +29,6 @@
2829
#include "ros2_medkit_gateway/core/managers/script_manager.hpp"
2930
#include "ros2_medkit_gateway/core/models/thread_safe_entity_cache.hpp"
3031
#include "ros2_medkit_gateway/core/plugins/plugin_manager.hpp"
31-
#include "ros2_medkit_gateway/discovery/discovery_manager.hpp"
3232
#include "ros2_medkit_gateway/dto/entities.hpp"
3333
#include "ros2_medkit_gateway/http/handlers/handler_support.hpp"
3434

‎src/ros2_medkit_gateway/src/http/handlers/health_handlers.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include "ros2_medkit_gateway/core/auth/auth_models.hpp"
2222
#include "ros2_medkit_gateway/core/data/topic_data_provider.hpp"
2323
#include "ros2_medkit_gateway/core/discovery/discovery_enums.hpp"
24+
#include "ros2_medkit_gateway/core/discovery/entity_discovery.hpp"
2425
#include "ros2_medkit_gateway/core/entity_validation.hpp"
2526
#include "ros2_medkit_gateway/core/http/error_codes.hpp"
2627
#include "ros2_medkit_gateway/core/http/fan_out_helpers.hpp"
@@ -33,7 +34,6 @@
3334
#include "ros2_medkit_gateway/core/models/thread_safe_entity_cache.hpp"
3435
#include "ros2_medkit_gateway/core/plugins/plugin_manager.hpp"
3536
#include "ros2_medkit_gateway/core/version.hpp"
36-
#include "ros2_medkit_gateway/discovery/discovery_manager.hpp"
3737
#include "ros2_medkit_gateway/dto/health.hpp"
3838

3939
#include "../../openapi/route_registry.hpp"

‎src/ros2_medkit_gateway/test/null_gateway_services.hpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class NullGatewayServices final : public GatewayServices {
3232
PluginManager * plugin_manager() override {
3333
return nullptr;
3434
}
35-
DiscoveryManager * discovery() override {
35+
EntityDiscovery * discovery() override {
3636
return nullptr;
3737
}
3838
ScriptManager * script_manager() override {

0 commit comments

Comments
 (0)