Skip to main content

ferrin_schema/
schema.rs

1//! The [`Schema`] abstraction: a JSON Schema plus a typed validator.
2
3use std::fmt;
4use std::sync::Arc;
5use std::sync::LazyLock;
6
7use ferrin_spec::error::TypeValidationError;
8use schemars::JsonSchema;
9use serde::de::DeserializeOwned;
10use serde_json::Value;
11use serde_json::json;
12
13use crate::dialect::SchemaDialect;
14use crate::transform::SchemaTransform;
15
16type LazyValue = LazyLock<Value, Box<dyn FnOnce() -> Value + Send>>;
17type Validate<T> = dyn Fn(Value) -> Result<T, TypeValidationError> + Send + Sync;
18
19/// A JSON Schema paired with a function that validates a JSON value and
20/// converts it into `T`.
21///
22/// The JSON Schema is produced lazily on first access and cached; cloning a
23/// schema shares the cache and the validator.
24pub struct Schema<T> {
25    json_schema: Arc<LazyValue>,
26    validate: Arc<Validate<T>>,
27}
28
29impl<T> Clone for Schema<T> {
30    fn clone(&self) -> Self {
31        Self {
32            json_schema: Arc::clone(&self.json_schema),
33            validate: Arc::clone(&self.validate),
34        }
35    }
36}
37
38impl<T> fmt::Debug for Schema<T> {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        f.debug_struct("Schema")
41            .field("json_schema", &LazyLock::get(&self.json_schema))
42            .finish_non_exhaustive()
43    }
44}
45
46impl<T: DeserializeOwned + JsonSchema + 'static> Schema<T> {
47    /// Derives the schema from `T` (draft-07, `additionalProperties: false`
48    /// on objects) and validates by deserializing into `T`.
49    #[must_use]
50    pub fn derived() -> Self {
51        Self::derived_with(SchemaDialect::default())
52    }
53
54    /// Like [`Schema::derived`] with an explicit dialect.
55    #[must_use]
56    pub fn derived_with(dialect: SchemaDialect) -> Self {
57        Self::lazy(
58            move || {
59                let mut schema = dialect.generate::<T>();
60                crate::transform::add_additional_properties_false(&mut schema);
61                schema
62            },
63            deserialize_into::<T>,
64        )
65    }
66}
67
68impl<T: DeserializeOwned + 'static> Schema<T> {
69    /// Uses a raw JSON Schema and validates by deserializing into `T`.
70    ///
71    /// With the `json-schema-validation` feature the value is first checked
72    /// against the JSON Schema, so constraints that `serde` cannot express
73    /// (ranges, patterns) are enforced as well.
74    #[must_use]
75    pub fn typed_from_json_schema(schema: Value) -> Self {
76        let dynamic = Schema::<Value>::from_json_schema(schema);
77        let dynamic_validate = Arc::clone(&dynamic.validate);
78        Self {
79            json_schema: dynamic.json_schema,
80            validate: Arc::new(move |value| {
81                let value = dynamic_validate(value)?;
82                deserialize_into::<T>(value)
83            }),
84        }
85    }
86}
87
88impl Schema<Value> {
89    /// Uses a raw JSON Schema; the value is returned unchanged after
90    /// validation.
91    ///
92    /// With the `json-schema-validation` feature the value is checked against
93    /// the schema (compiled lazily on first validation); without it every
94    /// value passes.
95    #[must_use]
96    pub fn from_json_schema(schema: Value) -> Self {
97        let json_schema = Arc::new(LazyValue::new(Box::new(move || schema)));
98        let validate = dynamic_validator(Arc::clone(&json_schema));
99        Self {
100            json_schema,
101            validate,
102        }
103    }
104
105    /// The empty object schema (`{type: object, properties: {},
106    /// additionalProperties: false}`) used when no schema is given.
107    #[must_use]
108    pub fn empty_object() -> Self {
109        Self::from_json_schema(json!({
110            "type": "object",
111            "properties": {},
112            "additionalProperties": false,
113        }))
114    }
115
116    /// A schema that accepts any JSON value.
117    #[must_use]
118    pub fn any() -> Self {
119        Self::from_json_schema(json!({}))
120    }
121}
122
123impl<T: 'static> Schema<T> {
124    /// Creates a schema from a lazily generated JSON Schema and a validator.
125    pub fn lazy(
126        json_schema: impl FnOnce() -> Value + Send + 'static,
127        validate: impl Fn(Value) -> Result<T, TypeValidationError> + Send + Sync + 'static,
128    ) -> Self {
129        Self {
130            json_schema: Arc::new(LazyValue::new(Box::new(json_schema))),
131            validate: Arc::new(validate),
132        }
133    }
134
135    /// Creates a schema from a JSON Schema value and a validator.
136    pub fn with_json_schema_and_validator(
137        json_schema: Value,
138        validate: impl Fn(Value) -> Result<T, TypeValidationError> + Send + Sync + 'static,
139    ) -> Self {
140        Self::lazy(move || json_schema, validate)
141    }
142
143    /// Replaces the validator, keeping the JSON Schema.
144    #[must_use]
145    pub fn with_validator(
146        self,
147        validate: impl Fn(Value) -> Result<T, TypeValidationError> + Send + Sync + 'static,
148    ) -> Self {
149        Self {
150            json_schema: self.json_schema,
151            validate: Arc::new(validate),
152        }
153    }
154
155    /// Returns a copy whose JSON Schema is rewritten by `transform`
156    /// immediately; validation is unchanged.
157    ///
158    /// # Errors
159    ///
160    /// Returns [`crate::SchemaError::UnsupportedTransform`] if the schema
161    /// cannot be represented by the requested transform.
162    pub fn transformed(&self, transform: SchemaTransform) -> Result<Self, crate::SchemaError> {
163        let transformed = transform.applied(self.json_schema().clone())?;
164        Ok(Self {
165            json_schema: Arc::new(LazyValue::new(Box::new(move || transformed))),
166            validate: Arc::clone(&self.validate),
167        })
168    }
169
170    /// Returns a `Schema<Value>` that runs this schema's validation but
171    /// yields the original JSON value.
172    #[must_use]
173    pub fn erased(&self) -> Schema<Value> {
174        let validate = Arc::clone(&self.validate);
175        Schema {
176            json_schema: Arc::clone(&self.json_schema),
177            validate: Arc::new(move |value| validate(value.clone()).map(|_| value)),
178        }
179    }
180}
181
182impl<T> Schema<T> {
183    /// Returns the JSON Schema, generating it on first access.
184    #[must_use]
185    pub fn json_schema(&self) -> &Value {
186        &self.json_schema
187    }
188
189    /// Validates `value` and converts it into `T`.
190    ///
191    /// # Errors
192    ///
193    /// Returns [`TypeValidationError`] when the value does not match.
194    pub fn validate(&self, value: Value) -> Result<T, TypeValidationError> {
195        (self.validate)(value)
196    }
197}
198
199fn deserialize_into<T: DeserializeOwned>(value: Value) -> Result<T, TypeValidationError> {
200    match serde_json::from_value::<T>(value.clone()) {
201        Ok(typed) => Ok(typed),
202        Err(error) => Err(TypeValidationError::new(value, error)),
203    }
204}
205
206#[cfg(feature = "json-schema-validation")]
207fn dynamic_validator(json_schema: Arc<LazyValue>) -> Arc<Validate<Value>> {
208    use std::sync::OnceLock;
209
210    use crate::validation::ValidationIssues;
211    use crate::validation::Validator;
212
213    let compiled: OnceLock<Result<Validator, String>> = OnceLock::new();
214    Arc::new(move |value| {
215        let validator = compiled
216            .get_or_init(|| Validator::compile(&json_schema).map_err(|error| error.to_string()));
217        match validator {
218            Ok(validator) => validator
219                .validate(&value)
220                .map(|()| value.clone())
221                .map_err(|issues| TypeValidationError::new(value, issues)),
222            Err(message) => Err(TypeValidationError::new(
223                value,
224                ValidationIssues::message(message.clone()),
225            )),
226        }
227    })
228}
229
230#[cfg(not(feature = "json-schema-validation"))]
231fn dynamic_validator(_json_schema: Arc<LazyValue>) -> Arc<Validate<Value>> {
232    Arc::new(Ok)
233}