Skip to main content

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