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}