use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TypedStruct {
pub type_url: String,
pub value: serde_json::Value,
}
#[derive(Debug)]
pub enum RegistryError<E> {
UnknownTypeUrl {
type_url: String,
available: Vec<String>,
},
Factory { type_url: String, source: E },
}
impl<E: fmt::Display> fmt::Display for RegistryError<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnknownTypeUrl {
type_url,
available,
} => write!(
f,
"unknown type URL '{}'. registered: [{}]",
type_url,
available.join(", ")
),
Self::Factory { type_url, source } => {
write!(f, "factory error for '{}': {}", type_url, source)
}
}
}
}
impl<E: fmt::Debug + fmt::Display> std::error::Error for RegistryError<E> {}
type BoxedFactory<T, E> =
Box<dyn Fn(&serde_json::Value) -> Result<T, E> + Send + Sync>;
pub struct TypedRegistryBuilder<T, E> {
factories: HashMap<String, BoxedFactory<T, E>>,
}
impl<T, E> TypedRegistryBuilder<T, E> {
pub fn new() -> Self {
Self {
factories: HashMap::new(),
}
}
#[must_use]
pub fn register(
mut self,
type_url: &str,
factory: impl Fn(&serde_json::Value) -> Result<T, E> + Send + Sync + 'static,
) -> Self {
self.factories
.insert(type_url.to_owned(), Box::new(factory));
self
}
#[must_use]
pub fn register_unique(
mut self,
type_url: &str,
factory: impl Fn(&serde_json::Value) -> Result<T, E> + Send + Sync + 'static,
) -> Self {
if self.factories.contains_key(type_url) {
panic!(
"duplicate type URL '{}' — each type URL must be registered exactly once.",
type_url
);
}
self.factories
.insert(type_url.to_owned(), Box::new(factory));
self
}
pub fn contains(&self, type_url: &str) -> bool {
self.factories.contains_key(type_url)
}
pub fn build(self) -> TypedRegistry<T, E> {
TypedRegistry {
factories: self.factories,
}
}
}
impl<T, E> Default for TypedRegistryBuilder<T, E> {
fn default() -> Self {
Self::new()
}
}
pub struct TypedRegistry<T, E> {
factories: HashMap<String, BoxedFactory<T, E>>,
}
impl<T, E> TypedRegistry<T, E> {
pub fn create(
&self,
type_url: &str,
value: &serde_json::Value,
) -> Result<T, RegistryError<E>> {
let factory = self.factories.get(type_url).ok_or_else(|| {
RegistryError::UnknownTypeUrl {
type_url: type_url.to_owned(),
available: self.type_urls_owned(),
}
})?;
factory(value).map_err(|source| RegistryError::Factory {
type_url: type_url.to_owned(),
source,
})
}
pub fn create_all(
&self,
entries: &[TypedStruct],
) -> Result<Vec<T>, RegistryError<E>> {
entries
.iter()
.map(|tc| self.create(&tc.type_url, &tc.value))
.collect()
}
pub fn type_urls(&self) -> Vec<&str> {
let mut urls: Vec<&str> = self.factories.keys().map(|s| s.as_str()).collect();
urls.sort_unstable();
urls
}
pub fn len(&self) -> usize {
self.factories.len()
}
pub fn is_empty(&self) -> bool {
self.factories.is_empty()
}
fn type_urls_owned(&self) -> Vec<String> {
let mut urls: Vec<String> = self.factories.keys().cloned().collect();
urls.sort_unstable();
urls
}
}
#[cfg(test)]
mod tests {
use super::*;
fn echo_factory(value: &serde_json::Value) -> Result<String, String> {
serde_json::from_value::<String>(value.clone()).map_err(|e| e.to_string())
}
fn int_factory(value: &serde_json::Value) -> Result<String, String> {
let n: i64 =
serde_json::from_value(value.clone()).map_err(|e| e.to_string())?;
Ok(format!("int:{n}"))
}
fn failing_factory(_value: &serde_json::Value) -> Result<String, String> {
Err("construction failed".to_owned())
}
#[test]
fn register_adds_factory() {
let registry = TypedRegistryBuilder::<String, String>::new()
.register("test.echo.v1", echo_factory)
.build();
assert_eq!(registry.len(), 1);
assert_eq!(registry.type_urls(), vec!["test.echo.v1"]);
}
#[test]
fn register_multiple() {
let registry = TypedRegistryBuilder::<String, String>::new()
.register("test.echo.v1", echo_factory)
.register("test.int.v1", int_factory)
.build();
assert_eq!(registry.len(), 2);
assert_eq!(
registry.type_urls(),
vec!["test.echo.v1", "test.int.v1"]
);
}
#[test]
fn register_duplicate_last_wins() {
let registry = TypedRegistryBuilder::<String, String>::new()
.register("test.v1", echo_factory)
.register("test.v1", int_factory)
.build();
assert_eq!(registry.len(), 1);
let result = registry
.create("test.v1", &serde_json::json!(42))
.unwrap();
assert_eq!(result, "int:42");
}
#[test]
#[should_panic(expected = "duplicate type URL")]
fn register_unique_panics_on_duplicate() {
let _ = TypedRegistryBuilder::<String, String>::new()
.register_unique("test.v1", echo_factory)
.register_unique("test.v1", int_factory)
.build();
}
#[test]
fn contains_checks_registration() {
let builder = TypedRegistryBuilder::<String, String>::new()
.register("test.echo.v1", echo_factory);
assert!(builder.contains("test.echo.v1"));
assert!(!builder.contains("test.missing.v1"));
}
#[test]
fn create_returns_instance() {
let registry = TypedRegistryBuilder::<String, String>::new()
.register("test.echo.v1", echo_factory)
.build();
let result = registry
.create("test.echo.v1", &serde_json::json!("hello"))
.unwrap();
assert_eq!(result, "hello");
}
#[test]
fn create_unknown_type_url() {
let registry = TypedRegistryBuilder::<String, String>::new()
.register("test.echo.v1", echo_factory)
.build();
let err = registry
.create("test.missing.v1", &serde_json::json!("x"))
.unwrap_err();
match err {
RegistryError::UnknownTypeUrl {
type_url,
available,
} => {
assert_eq!(type_url, "test.missing.v1");
assert_eq!(available, vec!["test.echo.v1"]);
}
_ => panic!("expected UnknownTypeUrl"),
}
}
#[test]
fn create_factory_error() {
let registry = TypedRegistryBuilder::<String, String>::new()
.register("test.fail.v1", failing_factory)
.build();
let err = registry
.create("test.fail.v1", &serde_json::json!(null))
.unwrap_err();
match err {
RegistryError::Factory { type_url, source } => {
assert_eq!(type_url, "test.fail.v1");
assert_eq!(source, "construction failed");
}
_ => panic!("expected Factory error"),
}
}
#[test]
fn create_all_success() {
let registry = TypedRegistryBuilder::<String, String>::new()
.register("test.echo.v1", echo_factory)
.register("test.int.v1", int_factory)
.build();
let configs = vec![
TypedStruct {
type_url: "test.echo.v1".into(),
value: serde_json::json!("hi"),
},
TypedStruct {
type_url: "test.int.v1".into(),
value: serde_json::json!(42),
},
];
let pipeline = registry.create_all(&configs).unwrap();
assert_eq!(pipeline, vec!["hi", "int:42"]);
}
#[test]
fn create_all_fails_on_unknown() {
let registry = TypedRegistryBuilder::<String, String>::new()
.register("test.echo.v1", echo_factory)
.build();
let configs = vec![
TypedStruct {
type_url: "test.echo.v1".into(),
value: serde_json::json!("hi"),
},
TypedStruct {
type_url: "test.missing.v1".into(),
value: serde_json::json!(null),
},
];
let err = registry.create_all(&configs).unwrap_err();
assert!(matches!(err, RegistryError::UnknownTypeUrl { .. }));
}
#[test]
fn empty_registry() {
let registry = TypedRegistryBuilder::<String, String>::new().build();
assert!(registry.is_empty());
assert_eq!(registry.len(), 0);
assert!(registry.type_urls().is_empty());
}
#[test]
fn typed_struct_deserializes() {
let json =
r#"{"type_url": "mox.geist.processors.v1.Test", "value": {"key": "value"}}"#;
let tc: TypedStruct = serde_json::from_str(json).unwrap();
assert_eq!(tc.type_url, "mox.geist.processors.v1.Test");
assert_eq!(tc.value["key"], "value");
}
#[test]
fn typed_struct_serializes_roundtrip() {
let tc = TypedStruct {
type_url: "test.v1".into(),
value: serde_json::json!({"a": 1}),
};
let json = serde_json::to_string(&tc).unwrap();
let tc2: TypedStruct = serde_json::from_str(&json).unwrap();
assert_eq!(tc2.type_url, "test.v1");
assert_eq!(tc2.value, serde_json::json!({"a": 1}));
}
#[test]
fn registry_error_display() {
let err: RegistryError<String> = RegistryError::UnknownTypeUrl {
type_url: "x.v1".into(),
available: vec!["a.v1".into(), "b.v1".into()],
};
assert_eq!(
err.to_string(),
"unknown type URL 'x.v1'. registered: [a.v1, b.v1]"
);
let err: RegistryError<String> = RegistryError::Factory {
type_url: "x.v1".into(),
source: "boom".into(),
};
assert_eq!(err.to_string(), "factory error for 'x.v1': boom");
}
}