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}