1use std::collections::HashMap;
7use std::fmt;
8
9#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
14pub struct TypedStruct {
15 pub type_url: String,
17 pub value: serde_json::Value,
19}
20
21#[derive(Debug)]
23pub enum RegistryError<E> {
24 UnknownTypeUrl {
26 type_url: String,
27 available: Vec<String>,
28 },
29 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
54type BoxedFactory<T, E> =
56 Box<dyn Fn(&serde_json::Value) -> Result<T, E> + Send + Sync>;
57
58pub 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 #[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 #[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 pub fn contains(&self, type_url: &str) -> bool {
105 self.factories.contains_key(type_url)
106 }
107
108 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
122pub struct TypedRegistry<T, E> {
127 factories: HashMap<String, BoxedFactory<T, E>>,
128}
129
130impl<T, E> TypedRegistry<T, E> {
131 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 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 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 pub fn len(&self) -> usize {
169 self.factories.len()
170 }
171
172 pub fn is_empty(&self) -> bool {
174 self.factories.is_empty()
175 }
176
177 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 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 #[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 #[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 #[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 #[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 #[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 #[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}