Skip to main content

hashtree_collection/
schema.rs

1use std::any::Any;
2use std::fmt;
3use std::sync::Arc;
4
5use crate::{CollectionDefinition, CollectionError};
6
7type CollectionTransformFn<T> = Arc<dyn Fn(&T) -> T + Send + Sync>;
8type CollectionValidateFn<T> = Arc<dyn Fn(&T) -> Result<(), CollectionError> + Send + Sync>;
9type CollectionMigrateFn<T> =
10    Arc<dyn Fn(Box<dyn Any>, u32) -> Result<T, CollectionError> + Send + Sync>;
11
12#[derive(Clone)]
13pub struct CollectionSchema<T> {
14    version: u32,
15    defaults: Option<CollectionTransformFn<T>>,
16    normalize: Option<CollectionTransformFn<T>>,
17    validate: Option<CollectionValidateFn<T>>,
18    migrate: Option<CollectionMigrateFn<T>>,
19}
20
21impl<T> CollectionSchema<T> {
22    pub fn new(version: u32) -> Self {
23        Self {
24            version,
25            defaults: None,
26            normalize: None,
27            validate: None,
28            migrate: None,
29        }
30    }
31
32    pub fn version(&self) -> u32 {
33        self.version
34    }
35
36    pub fn with_defaults(mut self, defaults: impl Fn(&T) -> T + Send + Sync + 'static) -> Self {
37        self.defaults = Some(Arc::new(defaults));
38        self
39    }
40
41    pub fn with_normalize(mut self, normalize: impl Fn(&T) -> T + Send + Sync + 'static) -> Self {
42        self.normalize = Some(Arc::new(normalize));
43        self
44    }
45
46    pub fn with_validate(
47        mut self,
48        validate: impl Fn(&T) -> Result<(), CollectionError> + Send + Sync + 'static,
49    ) -> Self {
50        self.validate = Some(Arc::new(validate));
51        self
52    }
53
54    pub fn with_migrate_from<Raw: 'static>(
55        mut self,
56        migrate: impl Fn(Raw, u32) -> Result<T, CollectionError> + Send + Sync + 'static,
57    ) -> Self {
58        self.migrate = Some(Arc::new(move |value, from_version| {
59            let raw = value.downcast::<Raw>().map_err(|_| {
60                CollectionError::Validation(format!(
61                    "collection schema migration expected value of type {}",
62                    std::any::type_name::<Raw>()
63                ))
64            })?;
65            migrate(*raw, from_version)
66        }));
67        self
68    }
69
70    pub(crate) fn defaults(&self) -> Option<&CollectionTransformFn<T>> {
71        self.defaults.as_ref()
72    }
73
74    pub(crate) fn normalize(&self) -> Option<&CollectionTransformFn<T>> {
75        self.normalize.as_ref()
76    }
77
78    pub(crate) fn validate(&self) -> Option<&CollectionValidateFn<T>> {
79        self.validate.as_ref()
80    }
81
82    pub(crate) fn migrate(&self) -> Option<&CollectionMigrateFn<T>> {
83        self.migrate.as_ref()
84    }
85}
86
87impl<T> fmt::Debug for CollectionSchema<T> {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        f.debug_struct("CollectionSchema")
90            .field("version", &self.version)
91            .finish()
92    }
93}
94
95#[derive(Debug, Clone, Default)]
96pub struct NormalizeCollectionItemOptions {
97    pub from_version: Option<u32>,
98}
99
100pub fn get_collection_schema<T>(
101    definition: &CollectionDefinition<T>,
102) -> Option<&CollectionSchema<T>> {
103    definition.schema()
104}
105
106pub fn get_schema_version<T>(definition: &CollectionDefinition<T>) -> u32 {
107    get_collection_schema(definition)
108        .map(CollectionSchema::version)
109        .unwrap_or(1)
110}
111
112pub fn normalize_collection_item<T: 'static, Raw: 'static>(
113    definition: &CollectionDefinition<T>,
114    value: Raw,
115    options: NormalizeCollectionItemOptions,
116) -> Result<T, CollectionError> {
117    let Some(schema) = get_collection_schema(definition) else {
118        return downcast_value(
119            value,
120            "collection item type does not match definition without schema migration",
121        );
122    };
123
124    let from_version = options.from_version.unwrap_or(schema.version());
125    let next = if from_version != schema.version() {
126        let migrate = schema.migrate().ok_or_else(|| {
127            CollectionError::Validation(format!(
128                "collection schema migration required: {} -> {}",
129                from_version,
130                schema.version()
131            ))
132        })?;
133        migrate(Box::new(value), from_version)?
134    } else {
135        downcast_value(
136            value,
137            "collection item type does not match definition for current schema version",
138        )?
139    };
140
141    apply_schema(schema, next)
142}
143
144pub(crate) fn normalize_typed_collection_item<T: Clone>(
145    definition: &CollectionDefinition<T>,
146    value: &T,
147) -> Result<T, CollectionError> {
148    let Some(schema) = get_collection_schema(definition) else {
149        return Ok(value.clone());
150    };
151    apply_schema(schema, value.clone())
152}
153
154fn apply_schema<T>(schema: &CollectionSchema<T>, mut value: T) -> Result<T, CollectionError> {
155    if let Some(defaults) = schema.defaults() {
156        value = defaults(&value);
157    }
158    if let Some(normalize) = schema.normalize() {
159        value = normalize(&value);
160    }
161    if let Some(validate) = schema.validate() {
162        validate(&value)?;
163    }
164    Ok(value)
165}
166
167fn downcast_value<T: 'static, Raw: 'static>(
168    value: Raw,
169    context: &str,
170) -> Result<T, CollectionError> {
171    let boxed: Box<dyn Any> = Box::new(value);
172    boxed
173        .downcast::<T>()
174        .map(|value| *value)
175        .map_err(|_| CollectionError::Validation(context.to_string()))
176}