Repository: OpenAssetIO/OpenAssetIO (language: python)

Issue:
Hybrid Plugin System
## What

Add a plugin system that combines multiple plugin systemss and dispatches based on advertised capability, or priority order if multiple factories advertise a capability.

## ACs

- Uses a shared identifier to locate the conceptual same manager across each plugin system.
- Make sure this is generic so it composes the output of N plugin systems.
- Uses `hasCapability` to route.
- Picks the first if present in both.

## Implementation notes

- Ideally have a internally `RoutingManagerInterface(vector<ManagerInterfacePtr> interfaces)` as this allows eg: Cpp, GPRC Python, in-process Python.




Discussion / hints:
https://lucascheller.github.io/VFX-UsdAssetResolver/resolvers/production.html
https://forum.aousd.org/t/batching-ar2-queries/818/2?u=foundrytom

Fix patch:
diff --git a/examples/manager/SimpleCppManager/src/SimpleCppManager.cpp b/examples/manager/SimpleCppManager/src/SimpleCppManager.cpp
index 5c7e69ec3..bd28a3d39 100644
--- a/examples/manager/SimpleCppManager/src/SimpleCppManager.cpp
+++ b/examples/manager/SimpleCppManager/src/SimpleCppManager.cpp
@@ -15,7 +15,9 @@
 #include <openassetio/trait/TraitsData.hpp>
 
 // Unique ID of the plugin.
-constexpr std::string_view kPluginId = "org.openassetio.examples.manager.simplecppmanager";
+constexpr std::string_view kDefaultPluginId = "org.openassetio.examples.manager.simplecppmanager";
+// Environment variable that can be used to override the plugin ID.
+constexpr std::string_view kPluginIdEnvVar = "OPENASSETIO_SIMPLECPPMANAGER_IDENTIFIER";
 // Settings keys.
 constexpr std::string_view kSettingsKeyForEntityRefPrefix = "prefix";
 constexpr std::string_view kSettingsKeyForCapabilities = "capabilities";
@@ -36,11 +38,19 @@ constexpr std::string_view kSettingsKeyForReadEntityTraitProperties = "read_trai
  * return a stub response, rather than throw a
  * `NotImplementedException`.
  *
+ * The unique identifier of this plugin can be customised using the
+ * OPENASSETIO_SIMPLECPPMANAGER_IDENTIFIER environment variable.
+ * Otherwise it is set to
+ * "org.openassetio.examples.manager.simplecppmanager"
+ *
  * @see initialize
  */
 struct SimpleCppManagerInterface final : openassetio::managerApi::ManagerInterface {
   [[nodiscard]] openassetio::Identifier identifier() const override {
-    return openassetio::Identifier{kPluginId};
+    if (const char* envVar = std::getenv(kPluginIdEnvVar.data())) {
+      return openassetio::Identifier{envVar};
+    }
+    return openassetio::Identifier{kDefaultPluginId};
   }
 
   /**
@@ -612,7 +622,10 @@ struct SimpleCppManagerInterface final : openassetio::managerApi::ManagerInterfa
  */
 struct Plugin final : openassetio::pluginSystem::CppPluginSystemManagerPlugin {
   [[nodiscard]] openassetio::Identifier identifier() const override {
-    return openassetio::Identifier{kPluginId};
+    if (const char* envVar = std::getenv(kPluginIdEnvVar.data())) {
+      return openassetio::Identifier{envVar};
+    }
+    return openassetio::Identifier{kDefaultPluginId};
   }
   openassetio::managerApi::ManagerInterfacePtr interface() override {
     return std::make_shared<SimpleCppManagerInterface>();


Test patch:
diff --git a/examples/manager/SimpleCppManager/tests/test_SimpleCppManager.py b/examples/manager/SimpleCppManager/tests/test_SimpleCppManager.py
index b91bfdf93..6264050ca 100644
--- a/examples/manager/SimpleCppManager/tests/test_SimpleCppManager.py
+++ b/examples/manager/SimpleCppManager/tests/test_SimpleCppManager.py
@@ -24,7 +24,6 @@
 # pylint: disable=invalid-name,redefined-outer-name
 # pylint: disable=missing-class-docstring,missing-function-docstring
 
-import os
 import pathlib
 
 import pytest
@@ -38,6 +37,43 @@
 from openassetio.log import ConsoleLogger
 
 
+class Test_SimpleCppManager_identifier:
+
+    def test_when_not_overridden_by_env_var_then_identifier_matches_default(
+        self, the_host_interface
+    ):
+        impl_factory = CppPluginSystemManagerImplementationFactory(ConsoleLogger())
+        manager_factory = ManagerFactory(the_host_interface, impl_factory, ConsoleLogger())
+
+        available_managers = manager_factory.availableManagers()
+
+        assert list(available_managers.keys()) == [
+            "org.openassetio.examples.manager.simplecppmanager"
+        ]
+
+        manager = manager_factory.createManager(
+            "org.openassetio.examples.manager.simplecppmanager"
+        )
+
+        assert manager.identifier() == "org.openassetio.examples.manager.simplecppmanager"
+
+    def test_when_overridden_by_env_var_then_identifier_matches_env_var(
+        self, monkeypatch, the_host_interface
+    ):
+        monkeypatch.setenv("OPENASSETIO_SIMPLECPPMANAGER_IDENTIFIER", "foo.bar")
+
+        impl_factory = CppPluginSystemManagerImplementationFactory(ConsoleLogger())
+        manager_factory = ManagerFactory(the_host_interface, impl_factory, ConsoleLogger())
+
+        available_managers = manager_factory.availableManagers()
+
+        assert list(available_managers.keys()) == ["foo.bar"]
+
+        manager = manager_factory.createManager("foo.bar")
+
+        assert manager.identifier() == "foo.bar"
+
+
 class Test_SimpleCppManager_initialize:
     class StubState(managerApi.ManagerStateBase):
         pass
@@ -383,7 +419,18 @@ def a_context(a_simple_cpp_manager):
 
 
 @pytest.fixture(scope="module")
-def the_args_for_manager_factory():
+def the_args_for_manager_factory(the_host_interface):
+    logger = ConsoleLogger()
+
+    impl_factory = CppPluginSystemManagerImplementationFactory(logger)
+
+    config_file_path = pathlib.Path(__file__).parent / "resources" / "openassetio_config.toml"
+
+    return (str(config_file_path), the_host_interface, impl_factory, logger)
+
+
+@pytest.fixture(scope="module")
+def the_host_interface():
     class TestHostInterface(HostInterface):
         def identifier(self):
             return "org.openassetio.examples.SimpleCppManager.test"
@@ -391,12 +438,4 @@ def identifier(self):
         def displayName(self):
             return "Simple C++ Manager Test Host"
 
-    logger = ConsoleLogger()
-
-    host_interface = TestHostInterface()
-
-    impl_factory = CppPluginSystemManagerImplementationFactory(logger)
-
-    config_file_path = pathlib.Path(__file__).parent / "resources" / "openassetio_config.toml"
-
-    return (str(config_file_path), host_interface, impl_factory, logger)
+    return TestHostInterface()