hashtree_collection/
schema.rs1use 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}