stygian_graph/application/
graphql_plugin_registry.rs

1//! Registry for named GraphQL target plugins.
2//!
3//! Plugins are registered at startup and looked up by name when the pipeline
4//! executor resolves a `kind = "graphql"` service that carries a
5//! `plugin = "<name>"` field.
6
7use std::collections::HashMap;
8use std::sync::Arc;
9
10use crate::domain::error::{ConfigError, Result, StygianError};
11use crate::ports::graphql_plugin::GraphQlTargetPlugin;
12
13/// A registry of named [`GraphQlTargetPlugin`] implementations.
14///
15/// # Example
16///
17/// ```rust
18/// use stygian_graph::application::graphql_plugin_registry::GraphQlPluginRegistry;
19/// use stygian_graph::ports::graphql_plugin::GraphQlTargetPlugin;
20/// use stygian_graph::ports::{GraphQlAuth, GraphQlAuthKind};
21/// use std::collections::HashMap;
22/// use std::sync::Arc;
23///
24/// struct DemoPlugin;
25/// impl GraphQlTargetPlugin for DemoPlugin {
26///     fn name(&self) -> &str { "demo" }
27///     fn endpoint(&self) -> &str { "https://demo.example.com/graphql" }
28/// }
29///
30/// let mut registry = GraphQlPluginRegistry::new();
31/// registry.register(Arc::new(DemoPlugin));
32/// let plugin = registry.get("demo").unwrap();
33/// assert_eq!(plugin.endpoint(), "https://demo.example.com/graphql");
34/// ```
35pub struct GraphQlPluginRegistry {
36    plugins: HashMap<String, Arc<dyn GraphQlTargetPlugin>>,
37}
38
39impl GraphQlPluginRegistry {
40    /// Create an empty registry.
41    pub fn new() -> Self {
42        Self {
43            plugins: HashMap::new(),
44        }
45    }
46
47    /// Register a plugin. Replaces any existing registration with the same name.
48    ///
49    /// # Example
50    ///
51    /// ```rust
52    /// use stygian_graph::application::graphql_plugin_registry::GraphQlPluginRegistry;
53    /// use stygian_graph::ports::graphql_plugin::GraphQlTargetPlugin;
54    /// use stygian_graph::ports::{GraphQlAuth, GraphQlAuthKind};
55    /// use std::collections::HashMap;
56    /// use std::sync::Arc;
57    ///
58    /// struct P;
59    /// impl GraphQlTargetPlugin for P {
60    ///     fn name(&self) -> &str { "p" }
61    ///     fn endpoint(&self) -> &str { "https://p.example.com/graphql" }
62    /// }
63    ///
64    /// let mut registry = GraphQlPluginRegistry::new();
65    /// registry.register(Arc::new(P));
66    /// ```
67    pub fn register(&mut self, plugin: Arc<dyn GraphQlTargetPlugin>) {
68        self.plugins.insert(plugin.name().to_owned(), plugin);
69    }
70
71    /// Look up a plugin by name.
72    ///
73    /// # Errors
74    ///
75    /// Returns [`StygianError::Config`] wrapping [`ConfigError::MissingConfig`]
76    /// if no plugin with that name has been registered.
77    ///
78    /// # Example
79    ///
80    /// ```rust
81    /// use stygian_graph::application::graphql_plugin_registry::GraphQlPluginRegistry;
82    /// use stygian_graph::ports::graphql_plugin::GraphQlTargetPlugin;
83    /// use stygian_graph::ports::{GraphQlAuth, GraphQlAuthKind};
84    /// use std::collections::HashMap;
85    /// use std::sync::Arc;
86    ///
87    /// struct P;
88    /// impl GraphQlTargetPlugin for P {
89    ///     fn name(&self) -> &str { "p" }
90    ///     fn endpoint(&self) -> &str { "https://p.example.com/graphql" }
91    /// }
92    ///
93    /// let mut registry = GraphQlPluginRegistry::new();
94    /// registry.register(Arc::new(P));
95    /// assert!(registry.get("p").is_ok());
96    /// assert!(registry.get("missing").is_err());
97    /// ```
98    pub fn get(&self, name: &str) -> Result<Arc<dyn GraphQlTargetPlugin>> {
99        self.plugins.get(name).cloned().ok_or_else(|| {
100            StygianError::Config(ConfigError::MissingConfig(format!(
101                "no GraphQL plugin registered for target '{name}'"
102            )))
103        })
104    }
105
106    /// List all registered plugin names.
107    ///
108    /// # Example
109    ///
110    /// ```rust
111    /// use stygian_graph::application::graphql_plugin_registry::GraphQlPluginRegistry;
112    /// use stygian_graph::ports::graphql_plugin::GraphQlTargetPlugin;
113    /// use stygian_graph::ports::{GraphQlAuth, GraphQlAuthKind};
114    /// use std::collections::HashMap;
115    /// use std::sync::Arc;
116    ///
117    /// struct P;
118    /// impl GraphQlTargetPlugin for P {
119    ///     fn name(&self) -> &str { "p" }
120    ///     fn endpoint(&self) -> &str { "https://p.example.com/graphql" }
121    /// }
122    ///
123    /// let mut registry = GraphQlPluginRegistry::new();
124    /// registry.register(Arc::new(P));
125    /// assert!(registry.list().contains(&"p"));
126    /// ```
127    pub fn list(&self) -> Vec<&str> {
128        self.plugins.keys().map(String::as_str).collect()
129    }
130}
131
132impl Default for GraphQlPluginRegistry {
133    fn default() -> Self {
134        Self::new()
135    }
136}
137
138#[cfg(test)]
139#[allow(clippy::unwrap_used)]
140mod tests {
141    use super::*;
142    use crate::ports::graphql_plugin::GraphQlTargetPlugin;
143
144    struct Plugin(&'static str, &'static str);
145
146    impl GraphQlTargetPlugin for Plugin {
147        fn name(&self) -> &str {
148            self.0
149        }
150        fn endpoint(&self) -> &str {
151            self.1
152        }
153    }
154
155    #[test]
156    fn register_and_get_plugin() {
157        let mut registry = GraphQlPluginRegistry::new();
158        registry.register(Arc::new(Plugin(
159            "jobber",
160            "https://api.getjobber.com/api/graphql",
161        )));
162        let plugin = registry.get("jobber").unwrap();
163        assert_eq!(plugin.endpoint(), "https://api.getjobber.com/api/graphql");
164    }
165
166    #[test]
167    fn get_unknown_plugin_returns_error() {
168        let registry = GraphQlPluginRegistry::new();
169        assert!(
170            matches!(registry.get("unknown"), Err(StygianError::Config(_))),
171            "expected Config error for unregistered plugin"
172        );
173    }
174
175    #[test]
176    fn register_overwrites_previous() {
177        let mut registry = GraphQlPluginRegistry::new();
178        registry.register(Arc::new(Plugin("api", "https://v1.example.com/graphql")));
179        registry.register(Arc::new(Plugin("api", "https://v2.example.com/graphql")));
180        let plugin = registry.get("api").unwrap();
181        assert_eq!(plugin.endpoint(), "https://v2.example.com/graphql");
182    }
183
184    #[test]
185    fn list_returns_all_names() {
186        let mut registry = GraphQlPluginRegistry::new();
187        registry.register(Arc::new(Plugin("alpha", "https://a.example.com/graphql")));
188        registry.register(Arc::new(Plugin("beta", "https://b.example.com/graphql")));
189        let mut names = registry.list();
190        names.sort_unstable();
191        assert_eq!(names, vec!["alpha", "beta"]);
192    }
193}