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 || SchemaTransform::AdditionalPropertiesFalse.applied(dialect.generate::<T>()),
59            deserialize_into::<T>,
60        )
61    }
62}
63
64impl<T: DeserializeOwned + 'static> Schema<T> {
65    /// Uses a raw JSON Schema and validates by deserializing into `T`.
66    ///
67    /// With the `json-schema-validation` feature the value is first checked
68    /// against the JSON Schema, so constraints that `serde` cannot express
69    /// (ranges, patterns) are enforced as well.
70    #[must_use]
71    pub fn typed_from_json_schema(schema: Value) -> Self {
72        let dynamic = Schema::<Value>::from_json_schema(schema);
73        let dynamic_validate = Arc::clone(&dynamic.validate);
74        Self {
75            json_schema: dynamic.json_schema,
76            validate: Arc::new(move |value| {
77                let value = dynamic_validate(value)?;
78                deserialize_into::<T>(value)
79            }),
80        }
81    }
82}
83
84impl Schema<Value> {
85    /// Uses a raw JSON Schema; the value is returned unchanged after
86    /// validation.
87    ///
88    /// With the `json-schema-validation` feature the value is checked against
89    /// the schema (compiled lazily on first validation); without it every
90    /// value passes.
91    #[must_use]
92    pub fn from_json_schema(schema: Value) -> Self {
93        let json_schema = Arc::new(LazyValue::new(Box::new(move || schema)));
94        let validate = dynamic_validator(Arc::clone(&json_schema));
95        Self {
96            json_schema,
97            validate,
98        }
99    }
100
101    /// The empty object schema (`{type: object, properties: {},
102    /// additionalProperties: false}`) used when no schema is given.
103    #[must_use]
104    pub fn empty_object() -> Self {
105        Self::from_json_schema(json!({
106            "type": "object",
107            "properties": {},
108            "additionalProperties": false,
109        }))
110    }
111
112    /// A schema that accepts any JSON value.
113    #[must_use]
114    pub fn any() -> Self {
115        Self::from_json_schema(json!({}))
116    }
117}
118
119impl<T: 'static> Schema<T> {
120    /// Creates a schema from a lazily generated JSON Schema and a validator.
121    pub fn lazy(
122        json_schema: impl FnOnce() -> Value + Send + 'static,
123        validate: impl Fn(Value) -> Result<T, TypeValidationError> + Send + Sync + 'static,
124    ) -> Self {
125        Self {
126            json_schema: Arc::new(LazyValue::new(Box::new(json_schema))),
127            validate: Arc::new(validate),
128        }
129    }
130
131    /// Creates a schema from a JSON Schema value and a validator.
132    pub fn with_json_schema_and_validator(
133        json_schema: Value,
134        validate: impl Fn(Value) -> Result<T, TypeValidationError> + Send + Sync + 'static,
135    ) -> Self {
136        Self::lazy(move || json_schema, validate)
137    }
138
139    /// Replaces the validator, keeping the JSON Schema.
140    #[must_use]
141    pub fn with_validator(
142        self,
143        validate: impl Fn(Value) -> Result<T, TypeValidationError> + Send + Sync + 'static,
144    ) -> Self {
145        Self {
146            json_schema: self.json_schema,
147            validate: Arc::new(validate),
148        }
149    }
150
151    /// Returns a copy whose JSON Schema is rewritten by `transform`
152    /// (lazily); validation is unchanged.
153    #[must_use]
154    pub fn transformed(&self, transform: SchemaTransform) -> Self {
155        let source = Arc::clone(&self.json_schema);
156        Self {
157            json_schema: Arc::new(LazyValue::new(Box::new(move || {
158                transform.applied((**source).clone())
159            }))),
160            validate: Arc::clone(&self.validate),
161        }
162    }
163
164    /// Returns a `Schema<Value>` that runs this schema's validation but
165    /// yields the original JSON value.
166    #[must_use]
167    pub fn erased(&self) -> Schema<Value> {
168        let validate = Arc::clone(&self.validate);
169        Schema {
170            json_schema: Arc::clone(&self.json_schema),
171            validate: Arc::new(move |value| validate(value.clone()).map(|_| value)),
172        }
173    }
174}
175
176impl<T> Schema<T> {
177    /// Returns the JSON Schema, generating it on first access.
178    #[must_use]
179    pub fn json_schema(&self) -> &Value {
180        &self.json_schema
181    }
182
183    /// Validates `value` and converts it into `T`.
184    ///
185    /// # Errors
186    ///
187    /// Returns [`TypeValidationError`] when the value does not match.
188    pub fn validate(&self, value: Value) -> Result<T, TypeValidationError> {
189        (self.validate)(value)
190    }
191}
192
193fn deserialize_into<T: DeserializeOwned>(value: Value) -> Result<T, TypeValidationError> {
194    match serde_json::from_value::<T>(value.clone()) {
195        Ok(typed) => Ok(typed),
196        Err(error) => Err(TypeValidationError::new(value, error)),
197    }
198}
199
200#[cfg(feature = "json-schema-validation")]
201fn dynamic_validator(json_schema: Arc<LazyValue>) -> Arc<Validate<Value>> {
202    use std::sync::OnceLock;
203
204    use crate::validation::ValidationIssues;
205    use crate::validation::Validator;
206
207    let compiled: OnceLock<Result<Validator, String>> = OnceLock::new();
208    Arc::new(move |value| {
209        let validator = compiled
210            .get_or_init(|| Validator::compile(&json_schema).map_err(|error| error.to_string()));
211        match validator {
212            Ok(validator) => validator
213                .validate(&value)
214                .map(|()| value.clone())
215                .map_err(|issues| TypeValidationError::new(value, issues)),
216            Err(message) => Err(TypeValidationError::new(
217                value,
218                ValidationIssues::message(message.clone()),
219            )),
220        }
221    })
222}
223
224#[cfg(not(feature = "json-schema-validation"))]
225fn dynamic_validator(_json_schema: Arc<LazyValue>) -> Arc<Validate<Value>> {
226    Arc::new(Ok)
227}