pub trait Named: Send + Sync + 'static {
fn name(&self) -> &str;
}
#[cfg(feature = "registry")]
mod registry {
use std::collections::BTreeMap;
use std::sync::Arc;
use arc_swap::ArcSwap;
use super::Named;
use crate::{ProximaError, RegistryError};
pub struct FactoryRegistry<F: ?Sized> {
factories: ArcSwap<BTreeMap<String, Arc<F>>>,
}
impl<F: ?Sized> Default for FactoryRegistry<F> {
fn default() -> Self {
Self {
factories: ArcSwap::from_pointee(BTreeMap::new()),
}
}
}
impl<F: Named + ?Sized> FactoryRegistry<F> {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn register(&self, factory: Arc<F>) -> Result<(), ProximaError> {
let name = factory.name().to_string();
loop {
let current = self.factories.load_full();
if current.contains_key(&name) {
return Err(RegistryError::AlreadyRegistered {
kind: "factory",
name,
}
.into());
}
let mut next: BTreeMap<String, Arc<F>> = (*current).clone();
next.insert(name.clone(), factory.clone());
let prev = self.factories.compare_and_swap(¤t, Arc::new(next));
if Arc::ptr_eq(&prev, ¤t) {
return Ok(());
}
}
}
pub fn with(self, factory: Arc<F>) -> Result<Self, ProximaError> {
self.register(factory)?;
Ok(self)
}
pub fn get(&self, name: &str) -> Result<Arc<F>, ProximaError> {
self.factories
.load_full()
.get(name)
.cloned()
.ok_or_else(|| {
RegistryError::NotRegistered {
kind: "factory",
name: name.to_string(),
}
.into()
})
}
#[must_use]
pub fn names(&self) -> Vec<String> {
self.factories.load_full().keys().cloned().collect()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
struct StubFactory {
registered_name: String,
}
impl Named for StubFactory {
fn name(&self) -> &str {
self.registered_name.as_str()
}
}
#[test]
fn register_get_and_names_round_trip() {
let registry: FactoryRegistry<dyn Named> = FactoryRegistry::new();
registry
.register(Arc::new(StubFactory {
registered_name: "alpha".into(),
}))
.expect("register");
registry
.register(Arc::new(StubFactory {
registered_name: "beta".into(),
}))
.expect("register");
assert_eq!(registry.get("alpha").expect("get").name(), "alpha");
assert_eq!(
registry.names(),
vec!["alpha".to_string(), "beta".to_string()]
);
}
#[test]
fn duplicate_name_is_a_registry_error() {
let registry: FactoryRegistry<dyn Named> = FactoryRegistry::new();
registry
.register(Arc::new(StubFactory {
registered_name: "dup".into(),
}))
.expect("first");
let outcome = registry.register(Arc::new(StubFactory {
registered_name: "dup".into(),
}));
assert!(matches!(
outcome,
Err(ProximaError::RegistryKind(
RegistryError::AlreadyRegistered { .. }
))
));
assert_eq!(
outcome
.expect_err("duplicate registration must fail")
.to_string(),
"registry: factory 'dup' already registered"
);
}
#[test]
fn fluent_with_chains_registration() {
let registry = FactoryRegistry::<dyn Named>::new()
.with(Arc::new(StubFactory {
registered_name: "a".into(),
}) as Arc<dyn Named>)
.expect("with a")
.with(Arc::new(StubFactory {
registered_name: "b".into(),
}) as Arc<dyn Named>)
.expect("with b");
assert_eq!(registry.names(), vec!["a".to_string(), "b".to_string()]);
}
#[test]
fn missing_name_is_a_registry_error() {
let registry: FactoryRegistry<dyn Named> = FactoryRegistry::new();
let outcome = registry.get("absent");
assert!(matches!(
outcome,
Err(ProximaError::RegistryKind(
RegistryError::NotRegistered { .. }
))
));
assert_eq!(
outcome
.err()
.expect("lookup of an absent name must fail")
.to_string(),
"registry: no factory named 'absent'"
);
}
}
}
#[cfg(feature = "registry")]
pub use registry::FactoryRegistry;
#[cfg(feature = "config")]
mod config {
use std::sync::Arc;
use bon::Builder;
use conflaguration::{Validate, ValidationMessage};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::{FactoryRegistry, Named};
use crate::{ConfigError, ProximaError};
pub trait Factory: Named {
type Output;
fn build(
&self,
spec: &Value,
children: Vec<Self::Output>,
) -> Result<Self::Output, ProximaError>;
}
#[derive(Serialize, Deserialize, Builder)]
#[builder(on(String, into))]
pub struct FactorySpec {
#[serde(rename = "type")]
pub kind: String,
#[serde(default)]
#[builder(default)]
pub spec: Value,
#[serde(default)]
#[builder(default)]
pub children: Vec<FactorySpec>,
}
#[derive(Default, Serialize, Deserialize, Builder)]
pub struct Composition {
#[serde(default, rename = "factory")]
#[builder(default)]
pub factories: Vec<FactorySpec>,
}
impl Validate for Composition {
fn validate(&self) -> conflaguration::Result<()> {
fn check(spec: &FactorySpec, errors: &mut Vec<ValidationMessage>) {
if spec.kind.trim().is_empty() {
errors.push(ValidationMessage::new(
"type",
"factory type must be non-empty",
));
}
for child in &spec.children {
check(child, errors);
}
}
let mut errors: Vec<ValidationMessage> = Vec::new();
for spec in &self.factories {
check(spec, &mut errors);
}
if errors.is_empty() {
Ok(())
} else {
Err(conflaguration::Error::Validation { errors })
}
}
}
impl Composition {
pub fn load(path: impl AsRef<std::path::Path>) -> Result<Self, ProximaError> {
conflaguration::ConfigBuilder::<Self>::new()
.file(path)
.validate()
.build()
.map_err(|err| {
ProximaError::from(ConfigError::Parse {
context: "composition",
source: Box::new(err),
})
})
}
}
impl<Output: 'static> FactoryRegistry<dyn Factory<Output = Output>> {
pub fn build(&self, composition: &Composition) -> Result<Vec<Output>, ProximaError> {
composition
.factories
.iter()
.map(|spec| self.build_spec(spec))
.collect()
}
fn build_spec(&self, spec: &FactorySpec) -> Result<Output, ProximaError> {
let children = spec
.children
.iter()
.map(|child| self.build_spec(child))
.collect::<Result<Vec<_>, _>>()?;
self.get(&spec.kind)?.build(&spec.spec, children)
}
pub fn register_factory<T>(&self, factory: T) -> Result<(), ProximaError>
where
T: Factory<Output = Output> + 'static,
{
self.register(Arc::new(factory))
}
pub fn with_factory<T>(self, factory: T) -> Result<Self, ProximaError>
where
T: Factory<Output = Output> + 'static,
{
self.with(Arc::new(factory))
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::RegistryError;
fn sample() -> Composition {
Composition::builder()
.factories(vec![
FactorySpec::builder()
.kind("tabs")
.children(vec![FactorySpec::builder().kind("chat").build()])
.build(),
FactorySpec::builder()
.kind("properties")
.spec(serde_json::json!({ "side": "toast" }))
.build(),
])
.build()
}
#[test]
fn fluent_and_serde_forms_are_identical() {
let json = serde_json::to_string(&sample()).expect("serialize");
let reloaded: Composition = serde_json::from_str(&json).expect("reload");
assert_eq!(
json,
serde_json::to_string(&reloaded).expect("reserialize"),
"the fluent and serialized forms must be identical"
);
}
#[test]
fn conflaguration_builder_validates_a_fluent_composition() {
let built = conflaguration::ConfigBuilder::<Composition>::new()
.value(sample())
.validate()
.build()
.expect("build");
assert_eq!(
serde_json::to_string(&built).expect("serialize built"),
serde_json::to_string(&sample()).expect("serialize sample"),
"value-seeded + validated build returns the fluent composition"
);
}
#[test]
fn validate_rejects_an_empty_kind_anywhere_in_the_tree() {
let bad = Composition::builder()
.factories(vec![
FactorySpec::builder()
.kind("ok")
.children(vec![FactorySpec::builder().kind(" ").build()])
.build(),
])
.build();
assert!(
bad.validate().is_err(),
"an empty factory type, even nested, is rejected"
);
}
struct TextFactory;
impl Named for TextFactory {
fn name(&self) -> &str {
"text"
}
}
impl Factory for TextFactory {
type Output = String;
fn build(&self, spec: &Value, _children: Vec<String>) -> Result<String, ProximaError> {
Ok(spec
.get("value")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string())
}
}
struct GroupFactory;
impl Named for GroupFactory {
fn name(&self) -> &str {
"group"
}
}
impl Factory for GroupFactory {
type Output = String;
fn build(&self, _spec: &Value, children: Vec<String>) -> Result<String, ProximaError> {
Ok(format!("[{}]", children.join(",")))
}
}
#[test]
fn build_drives_a_composition_tree_in_one_call() {
let registry = FactoryRegistry::<dyn Factory<Output = String>>::new()
.with_factory(TextFactory)
.expect("text")
.with_factory(GroupFactory)
.expect("group");
let composition = Composition::builder()
.factories(vec![
FactorySpec::builder()
.kind("group")
.children(vec![
FactorySpec::builder()
.kind("text")
.spec(serde_json::json!({ "value": "ask" }))
.build(),
FactorySpec::builder()
.kind("text")
.spec(serde_json::json!({ "value": "browse" }))
.build(),
])
.build(),
])
.build();
let rendered = registry.build(&composition).expect("build composition");
assert_eq!(
rendered,
vec!["[ask,browse]".to_string()],
"children build depth-first, then the group wraps them"
);
}
#[test]
fn build_surfaces_an_unknown_kind_as_a_registry_error() {
let registry: FactoryRegistry<dyn Factory<Output = String>> = FactoryRegistry::new();
let composition = Composition::builder()
.factories(vec![FactorySpec::builder().kind("absent").build()])
.build();
assert!(matches!(
registry.build(&composition),
Err(ProximaError::RegistryKind(
RegistryError::NotRegistered { .. }
))
));
}
}
}
#[cfg(feature = "config")]
pub use config::{Composition, Factory, FactorySpec};