Skip to main content

slick/
registry.rs

1//! Generic typed extension registry.
2//!
3//! Maps type URL → factory closure → typed instance. Builder pattern
4//! ensures the registry is immutable after construction.
5
6use std::collections::HashMap;
7use std::fmt;
8
9/// Typed structured data envelope — a type URL plus an opaque value.
10///
11/// Isomorphic to xDS `TypedStruct`: a type URL that identifies the schema,
12/// and a structured value interpreted by the consumer (factory, runtime, etc.).
13#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
14pub struct TypedStruct {
15    /// Type URL identifying the schema (e.g. `mox.geist.processors.v1.AccessControl`).
16    pub type_url: String,
17    /// Opaque structured value — interpretation determined by the type_url consumer.
18    pub value: serde_json::Value,
19}
20
21/// Error returned by [`TypedRegistry::create`] and [`TypedRegistry::create_all`].
22#[derive(Debug)]
23pub enum RegistryError<E> {
24    /// No factory registered for this type URL.
25    UnknownTypeUrl {
26        type_url: String,
27        available: Vec<String>,
28    },
29    /// Factory returned an error during instantiation.
30    Factory { type_url: String, source: E },
31}
32
33impl<E: fmt::Display> fmt::Display for RegistryError<E> {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        match self {
36            Self::UnknownTypeUrl {
37                type_url,
38                available,
39            } => write!(
40                f,
41                "unknown type URL '{}'. registered: [{}]",
42                type_url,
43                available.join(", ")
44            ),
45            Self::Factory { type_url, source } => {
46                write!(f, "factory error for '{}': {}", type_url, source)
47            }
48        }
49    }
50}
51
52impl<E: fmt::Debug + fmt::Display> std::error::Error for RegistryError<E> {}
53
54// Type-erased factory stored in the registry.
55type BoxedFactory<T, E> =
56    Box<dyn Fn(&serde_json::Value) -> Result<T, E> + Send + Sync>;
57
58/// Mutable builder for [`TypedRegistry`]. Immutable after [`build()`](Self::build).
59pub struct TypedRegistryBuilder<T, E> {
60    factories: HashMap<String, BoxedFactory<T, E>>,
61}
62
63impl<T, E> TypedRegistryBuilder<T, E> {
64    pub fn new() -> Self {
65        Self {
66            factories: HashMap::new(),
67        }
68    }
69
70    /// Register a factory for a type URL. Last registration wins on duplicate.
71    #[must_use]
72    pub fn register(
73        mut self,
74        type_url: &str,
75        factory: impl Fn(&serde_json::Value) -> Result<T, E> + Send + Sync + 'static,
76    ) -> Self {
77        self.factories
78            .insert(type_url.to_owned(), Box::new(factory));
79        self
80    }
81
82    /// Register a factory, panicking if the type URL is already registered.
83    ///
84    /// Use this for distributed registration (e.g. `inventory::submit!`)
85    /// where duplicates indicate a configuration error.
86    #[must_use]
87    pub fn register_unique(
88        mut self,
89        type_url: &str,
90        factory: impl Fn(&serde_json::Value) -> Result<T, E> + Send + Sync + 'static,
91    ) -> Self {
92        if self.factories.contains_key(type_url) {
93            panic!(
94                "duplicate type URL '{}' — each type URL must be registered exactly once.",
95                type_url
96            );
97        }
98        self.factories
99            .insert(type_url.to_owned(), Box::new(factory));
100        self
101    }
102
103    /// Returns true if a factory is registered for this type URL.
104    pub fn contains(&self, type_url: &str) -> bool {
105        self.factories.contains_key(type_url)
106    }
107
108    /// Freeze the registry. No further registrations possible.
109    pub fn build(self) -> TypedRegistry<T, E> {
110        TypedRegistry {
111            factories: self.factories,
112        }
113    }
114}
115
116impl<T, E> Default for TypedRegistryBuilder<T, E> {
117    fn default() -> Self {
118        Self::new()
119    }
120}
121
122/// Immutable typed registry. Maps type URL → factory.
123///
124/// Created via [`TypedRegistryBuilder::build`]. Thread-safe and
125/// shareable via `Arc`.
126pub struct TypedRegistry<T, E> {
127    factories: HashMap<String, BoxedFactory<T, E>>,
128}
129
130impl<T, E> TypedRegistry<T, E> {
131    /// Instantiate a value from a type URL and config.
132    pub fn create(
133        &self,
134        type_url: &str,
135        value: &serde_json::Value,
136    ) -> Result<T, RegistryError<E>> {
137        let factory = self.factories.get(type_url).ok_or_else(|| {
138            RegistryError::UnknownTypeUrl {
139                type_url: type_url.to_owned(),
140                available: self.type_urls_owned(),
141            }
142        })?;
143        factory(value).map_err(|source| RegistryError::Factory {
144            type_url: type_url.to_owned(),
145            source,
146        })
147    }
148
149    /// Instantiate values from a list of typed struct entries.
150    pub fn create_all(
151        &self,
152        entries: &[TypedStruct],
153    ) -> Result<Vec<T>, RegistryError<E>> {
154        entries
155            .iter()
156            .map(|tc| self.create(&tc.type_url, &tc.value))
157            .collect()
158    }
159
160    /// List all registered type URLs, sorted (for diagnostics).
161    pub fn type_urls(&self) -> Vec<&str> {
162        let mut urls: Vec<&str> = self.factories.keys().map(|s| s.as_str()).collect();
163        urls.sort_unstable();
164        urls
165    }
166
167    /// Returns the number of registered factories.
168    pub fn len(&self) -> usize {
169        self.factories.len()
170    }
171
172    /// Returns true if no factories are registered.
173    pub fn is_empty(&self) -> bool {
174        self.factories.is_empty()
175    }
176
177    /// Sorted owned type URLs (for error messages).
178    fn type_urls_owned(&self) -> Vec<String> {
179        let mut urls: Vec<String> = self.factories.keys().cloned().collect();
180        urls.sort_unstable();
181        urls
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    // -- Helpers --
190
191    fn echo_factory(value: &serde_json::Value) -> Result<String, String> {
192        serde_json::from_value::<String>(value.clone()).map_err(|e| e.to_string())
193    }
194
195    fn int_factory(value: &serde_json::Value) -> Result<String, String> {
196        let n: i64 =
197            serde_json::from_value(value.clone()).map_err(|e| e.to_string())?;
198        Ok(format!("int:{n}"))
199    }
200
201    fn failing_factory(_value: &serde_json::Value) -> Result<String, String> {
202        Err("construction failed".to_owned())
203    }
204
205    // -- Builder tests --
206
207    #[test]
208    fn register_adds_factory() {
209        let registry = TypedRegistryBuilder::<String, String>::new()
210            .register("test.echo.v1", echo_factory)
211            .build();
212
213        assert_eq!(registry.len(), 1);
214        assert_eq!(registry.type_urls(), vec!["test.echo.v1"]);
215    }
216
217    #[test]
218    fn register_multiple() {
219        let registry = TypedRegistryBuilder::<String, String>::new()
220            .register("test.echo.v1", echo_factory)
221            .register("test.int.v1", int_factory)
222            .build();
223
224        assert_eq!(registry.len(), 2);
225        assert_eq!(
226            registry.type_urls(),
227            vec!["test.echo.v1", "test.int.v1"]
228        );
229    }
230
231    #[test]
232    fn register_duplicate_last_wins() {
233        let registry = TypedRegistryBuilder::<String, String>::new()
234            .register("test.v1", echo_factory)
235            .register("test.v1", int_factory)
236            .build();
237
238        assert_eq!(registry.len(), 1);
239        let result = registry
240            .create("test.v1", &serde_json::json!(42))
241            .unwrap();
242        assert_eq!(result, "int:42");
243    }
244
245    #[test]
246    #[should_panic(expected = "duplicate type URL")]
247    fn register_unique_panics_on_duplicate() {
248        let _ = TypedRegistryBuilder::<String, String>::new()
249            .register_unique("test.v1", echo_factory)
250            .register_unique("test.v1", int_factory)
251            .build();
252    }
253
254    #[test]
255    fn contains_checks_registration() {
256        let builder = TypedRegistryBuilder::<String, String>::new()
257            .register("test.echo.v1", echo_factory);
258
259        assert!(builder.contains("test.echo.v1"));
260        assert!(!builder.contains("test.missing.v1"));
261    }
262
263    // -- Create tests --
264
265    #[test]
266    fn create_returns_instance() {
267        let registry = TypedRegistryBuilder::<String, String>::new()
268            .register("test.echo.v1", echo_factory)
269            .build();
270
271        let result = registry
272            .create("test.echo.v1", &serde_json::json!("hello"))
273            .unwrap();
274        assert_eq!(result, "hello");
275    }
276
277    #[test]
278    fn create_unknown_type_url() {
279        let registry = TypedRegistryBuilder::<String, String>::new()
280            .register("test.echo.v1", echo_factory)
281            .build();
282
283        let err = registry
284            .create("test.missing.v1", &serde_json::json!("x"))
285            .unwrap_err();
286        match err {
287            RegistryError::UnknownTypeUrl {
288                type_url,
289                available,
290            } => {
291                assert_eq!(type_url, "test.missing.v1");
292                assert_eq!(available, vec!["test.echo.v1"]);
293            }
294            _ => panic!("expected UnknownTypeUrl"),
295        }
296    }
297
298    #[test]
299    fn create_factory_error() {
300        let registry = TypedRegistryBuilder::<String, String>::new()
301            .register("test.fail.v1", failing_factory)
302            .build();
303
304        let err = registry
305            .create("test.fail.v1", &serde_json::json!(null))
306            .unwrap_err();
307        match err {
308            RegistryError::Factory { type_url, source } => {
309                assert_eq!(type_url, "test.fail.v1");
310                assert_eq!(source, "construction failed");
311            }
312            _ => panic!("expected Factory error"),
313        }
314    }
315
316    // -- Pipeline tests --
317
318    #[test]
319    fn create_all_success() {
320        let registry = TypedRegistryBuilder::<String, String>::new()
321            .register("test.echo.v1", echo_factory)
322            .register("test.int.v1", int_factory)
323            .build();
324
325        let configs = vec![
326            TypedStruct {
327                type_url: "test.echo.v1".into(),
328                value: serde_json::json!("hi"),
329            },
330            TypedStruct {
331                type_url: "test.int.v1".into(),
332                value: serde_json::json!(42),
333            },
334        ];
335
336        let pipeline = registry.create_all(&configs).unwrap();
337        assert_eq!(pipeline, vec!["hi", "int:42"]);
338    }
339
340    #[test]
341    fn create_all_fails_on_unknown() {
342        let registry = TypedRegistryBuilder::<String, String>::new()
343            .register("test.echo.v1", echo_factory)
344            .build();
345
346        let configs = vec![
347            TypedStruct {
348                type_url: "test.echo.v1".into(),
349                value: serde_json::json!("hi"),
350            },
351            TypedStruct {
352                type_url: "test.missing.v1".into(),
353                value: serde_json::json!(null),
354            },
355        ];
356
357        let err = registry.create_all(&configs).unwrap_err();
358        assert!(matches!(err, RegistryError::UnknownTypeUrl { .. }));
359    }
360
361    // -- Empty registry --
362
363    #[test]
364    fn empty_registry() {
365        let registry = TypedRegistryBuilder::<String, String>::new().build();
366        assert!(registry.is_empty());
367        assert_eq!(registry.len(), 0);
368        assert!(registry.type_urls().is_empty());
369    }
370
371    // -- TypedStruct deserialization --
372
373    #[test]
374    fn typed_struct_deserializes() {
375        let json =
376            r#"{"type_url": "mox.geist.processors.v1.Test", "value": {"key": "value"}}"#;
377        let tc: TypedStruct = serde_json::from_str(json).unwrap();
378        assert_eq!(tc.type_url, "mox.geist.processors.v1.Test");
379        assert_eq!(tc.value["key"], "value");
380    }
381
382    #[test]
383    fn typed_struct_serializes_roundtrip() {
384        let tc = TypedStruct {
385            type_url: "test.v1".into(),
386            value: serde_json::json!({"a": 1}),
387        };
388        let json = serde_json::to_string(&tc).unwrap();
389        let tc2: TypedStruct = serde_json::from_str(&json).unwrap();
390        assert_eq!(tc2.type_url, "test.v1");
391        assert_eq!(tc2.value, serde_json::json!({"a": 1}));
392    }
393
394    // -- Display --
395
396    #[test]
397    fn registry_error_display() {
398        let err: RegistryError<String> = RegistryError::UnknownTypeUrl {
399            type_url: "x.v1".into(),
400            available: vec!["a.v1".into(), "b.v1".into()],
401        };
402        assert_eq!(
403            err.to_string(),
404            "unknown type URL 'x.v1'. registered: [a.v1, b.v1]"
405        );
406
407        let err: RegistryError<String> = RegistryError::Factory {
408            type_url: "x.v1".into(),
409            source: "boom".into(),
410        };
411        assert_eq!(err.to_string(), "factory error for 'x.v1': boom");
412    }
413}