Skip to main content

shape/
lib.rs

1//! # shape
2//!
3//! Schema by example: a spec is written as an example of the value it
4//! accepts, and validating a value against it completes it. The Rust port of
5//! [shape](https://github.com/rjrodger/shape); the TypeScript implementation
6//! is canonical and this one is held to it by a shared corpus.
7//!
8//! ```
9//! use shape::{obj, shape, Spec, Token, Value};
10//!
11//! let s = shape(obj([("name", Spec::from(Token::String)), ("port", Spec::from(8080))]));
12//! let out = s.validate(Value::from(serde_json::json!({ "name": "x" }))).unwrap();
13//! assert_eq!(serde_json::Value::from(out), serde_json::json!({ "name": "x", "port": 8080 }));
14//! ```
15
16pub mod algebra;
17pub mod argu;
18pub mod builders;
19pub mod coerce;
20pub mod context;
21pub mod discriminated;
22pub mod error;
23pub mod expr;
24pub mod format;
25pub mod isolate;
26pub mod json;
27pub mod jsonschema;
28pub mod jsonschema_import;
29mod macros;
30pub mod node;
31pub mod normalize;
32mod regexp;
33pub mod spec;
34pub mod standard;
35pub mod stringify;
36pub mod validate;
37pub mod value;
38
39pub use algebra::{extend, omit, partial, pick, Names};
40pub use argu::{Argu, ArguError, Signature};
41pub use builders::*;
42pub use context::{Context, PathPart, State, Update, UpdateErr};
43pub use discriminated::{discriminated, Disc};
44pub use error::{FieldError, ValidationError};
45pub use expr::build;
46pub use expr::{expr, expr_apply, ExprError};
47pub use isolate::{Inner, TransformFn};
48pub use json::{node_json, JsonError};
49pub use jsonschema::json_schema;
50pub use jsonschema_import::{from_json_schema, JsonSchemaError};
51pub use node::{Kind, ListMode, Node, Token, Validator, ValidatorFn};
52pub use normalize::Options;
53pub use spec::{arr, from_map, null, obj, Spec};
54pub use standard::{StandardIssue, StandardResult, StandardSchema};
55pub use stringify::stringify_node;
56pub use value::{Map, Value};
57
58use std::collections::HashMap;
59use std::sync::Arc;
60use validate::{validate_ignored, validate_node, Cur, Walk};
61
62/// The crate version.
63pub const VERSION: &str = env!("CARGO_PKG_VERSION");
64
65/// A compiled spec.
66#[derive(Clone, Debug)]
67pub struct Schema {
68    root: Node,
69    /// The `define`d nodes by name, for `refer`.
70    defs: Arc<HashMap<String, Arc<Node>>>,
71    /// No validator anywhere in the tree: a verdict needs no path.
72    pure: bool,
73}
74
75/// Compile a spec.
76pub fn shape(spec: impl Into<Spec>) -> Schema {
77    Schema::new(spec)
78}
79
80impl Schema {
81    pub fn new(spec: impl Into<Spec>) -> Schema {
82        Schema::with_options(spec, &Options::default())
83    }
84
85    /// Compile a spec, reading it as the options say.
86    pub fn with_options(spec: impl Into<Spec>, opts: &Options) -> Schema {
87        let mut root = normalize::normalize_with(spec.into(), opts);
88        let mut defs = HashMap::new();
89        let pure = prepare(&mut root, &mut defs);
90        Schema {
91            root,
92            defs: Arc::new(defs),
93            pure,
94        }
95    }
96
97    /// Compile the string form of a spec: `Schema::parse("String.Min(2)")`.
98    pub fn parse(src: &str) -> Result<Schema, ExprError> {
99        Ok(Schema::new(expr(src)?))
100    }
101
102    /// The schema as a JSON Schema document (draft 2020-12).
103    /// The declarative JSON of the shape, which [`crate::expr::build`]
104    /// reads back.
105    pub fn json(&self) -> Result<Value, crate::json::JsonError> {
106        crate::json::node_json(self.node())
107    }
108
109    pub fn json_schema(&self) -> Value {
110        json_schema(&self.root)
111    }
112
113    /// The `define`d nodes of the schema, by name.
114    pub fn defs(&self) -> &HashMap<String, Arc<Node>> {
115        &self.defs
116    }
117
118    /// The compiled tree.
119    pub fn node(&self) -> &Node {
120        &self.root
121    }
122
123    /// Validate and complete a value: defaults injected, children validated.
124    /// `Value::Undefined` is no value at all, as a bare `shape()` call is.
125    pub fn validate(&self, input: Value) -> Result<Value, ValidationError> {
126        let mut ctx = Context::new();
127        self.validate_ctx(input, &mut ctx)
128    }
129
130    /// `validate` with a context of the caller's, for custom validators.
131    pub fn validate_ctx(&self, input: Value, ctx: &mut Context) -> Result<Value, ValidationError> {
132        let mut value = input;
133        let mut verr = ValidationError::default();
134        ctx.terse = false;
135        let kept = {
136            let mut w = Walk {
137                ctx,
138                defs: &self.defs,
139                is_match: false,
140                path: Vec::new(),
141                paths: true,
142            };
143            walk_root(&self.root, Cur::Mut(&mut value), &mut w, &mut verr)
144        };
145        ctx.err.extend(verr.issues.iter().cloned());
146        if verr.has_any() {
147            return Err(verr);
148        }
149        if !kept {
150            value = Value::Undefined;
151        }
152        Ok(value)
153    }
154
155    /// Whether the value validates. Nothing is produced or rendered.
156    pub fn valid(&self, input: &Value) -> bool {
157        let mut ctx = Context::new();
158        ctx.terse = true;
159        let mut verr = ValidationError {
160            terse: true,
161            ..Default::default()
162        };
163        let mut w = Walk {
164            ctx: &mut ctx,
165            defs: &self.defs,
166            is_match: true,
167            path: Vec::new(),
168            paths: !self.pure,
169        };
170        walk_root(&self.root, Cur::Ref(input), &mut w, &mut verr);
171        !verr.has_any()
172    }
173
174    /// `valid` by another name.
175    pub fn matches(&self, input: &Value) -> bool {
176        self.valid(input)
177    }
178
179    /// The errors of validating the value; empty when it validates.
180    pub fn error(&self, input: &Value) -> Vec<FieldError> {
181        let mut ctx = Context::new();
182        let mut verr = ValidationError::default();
183        let mut w = Walk {
184            ctx: &mut ctx,
185            defs: &self.defs,
186            is_match: true,
187            path: Vec::new(),
188            paths: true,
189        };
190        walk_root(&self.root, Cur::Ref(input), &mut w, &mut verr);
191        verr.issues
192    }
193}
194
195/// Prepare a node outside a schema, as Argu compiles its slots.
196pub(crate) fn prepare_node(n: &mut Node) {
197    let mut defs = HashMap::new();
198    prepare(n, &mut defs);
199}
200
201/// Walk a compiled tree once: every object node gets the set of keys it
202/// accepts and its keys to share with the paths, and every `define`d node
203/// is collected by name. Reports whether the tree has no validator at all.
204fn prepare(n: &mut Node, defs: &mut HashMap<String, Arc<Node>>) -> bool {
205    let mut pure = n.befores.is_empty() && n.afters.is_empty();
206    n.plain = pure
207        && !n.silent
208        && n.rename_to.is_none()
209        && n.rename_claim.is_empty()
210        && n.regexp.is_none()
211        && n.list_mode == ListMode::None
212        && n.disc.is_none();
213    if n.kind == Kind::Object {
214        n.obj_keys = n
215            .obj_children
216            .keys()
217            .map(|k| Arc::from(k.as_str()))
218            .collect();
219        let mut consumed = std::collections::HashSet::with_capacity(n.obj_children.len());
220        for (k, cn) in &n.obj_children {
221            consumed.insert(k.clone());
222            if let Some(to) = &cn.rename_to {
223                consumed.insert(to.clone());
224            }
225            for src in &cn.rename_claim {
226                consumed.insert(src.clone());
227            }
228        }
229        n.consumed = consumed;
230    }
231    for cn in n.obj_children.values_mut() {
232        pure &= prepare(cn, defs);
233    }
234    if let Some(r) = n.obj_rest.as_deref_mut() {
235        pure &= prepare(r, defs);
236    }
237    for cn in n.arr_children.iter_mut() {
238        pure &= prepare(cn, defs);
239    }
240    if let Some(c) = n.arr_child.as_deref_mut() {
241        pure &= prepare(c, defs);
242    }
243    if let Some(r) = n.arr_rest.as_deref_mut() {
244        pure &= prepare(r, defs);
245    }
246    for sn in n.list.iter_mut() {
247        pure &= prepare(sn, defs);
248    }
249    if let Some(name) = &n.define_name {
250        defs.insert(name.clone(), Arc::new(n.clone()));
251    }
252    pure
253}
254
255/// What `validate_into` can fail with: the validation, or the deserialization
256/// of the produced value into the target type.
257#[cfg(feature = "serde")]
258#[derive(Debug)]
259pub enum IntoError {
260    Validation(ValidationError),
261    Deserialize(serde_json::Error),
262}
263
264#[cfg(feature = "serde")]
265impl std::fmt::Display for IntoError {
266    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267        match self {
268            IntoError::Validation(e) => write!(f, "{}", e),
269            IntoError::Deserialize(e) => write!(f, "{}", e),
270        }
271    }
272}
273
274#[cfg(feature = "serde")]
275impl std::error::Error for IntoError {}
276
277#[cfg(feature = "serde")]
278impl Schema {
279    /// Validate, and deserialize the produced value into a type.
280    pub fn validate_into<T: serde::de::DeserializeOwned>(
281        &self,
282        input: impl Into<Value>,
283    ) -> Result<T, IntoError> {
284        let out = self.validate(input.into()).map_err(IntoError::Validation)?;
285        serde_json::from_value(serde_json::Value::from(out)).map_err(IntoError::Deserialize)
286    }
287}
288
289/// The root honours `Ignore` as a property or an element does: a value that
290/// does not validate is dropped along with its errors.
291fn walk_root(root: &Node, cur: Cur<'_>, w: &mut Walk<'_>, verr: &mut ValidationError) -> bool {
292    if root.is_ignore() {
293        validate_ignored(root, cur, "", false, w, verr)
294    } else {
295        validate_node(root, cur, "", false, w, verr)
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    #[test]
304    fn api() {
305        let s = shape(1);
306        assert_eq!(s.validate(Value::Undefined).unwrap(), Value::Num(1.0));
307        assert!(s.matches(&Value::Num(2.0)));
308        assert!(!s.matches(&Value::Str("x".into())));
309        assert_eq!(s.node().kind, Kind::Number);
310        assert!(!VERSION.is_empty());
311    }
312}
313
314#[cfg(all(test, feature = "serde"))]
315mod serde_tests {
316    use super::*;
317    use serde::Deserialize;
318
319    #[derive(Deserialize, Debug, PartialEq)]
320    struct Config {
321        name: String,
322        port: u16,
323    }
324
325    #[test]
326    fn validates_into_a_type() {
327        let s = shape(obj([
328            ("name", Spec::from(Token::String)),
329            ("port", Spec::from(8080)),
330        ]));
331        let c: Config = s
332            .validate_into(Value::from(serde_json::json!({"name": "x"})))
333            .unwrap();
334        assert_eq!(
335            c,
336            Config {
337                name: "x".into(),
338                port: 8080
339            }
340        );
341        let bad = s.validate_into::<Config>(Value::from(serde_json::json!({"name": 1})));
342        assert!(matches!(bad, Err(IntoError::Validation(_))));
343        assert!(bad.unwrap_err().to_string().contains("not of type string"));
344        let s = shape(obj([
345            ("name", Spec::from(Token::String)),
346            ("port", Spec::from(-1)),
347        ]));
348        let bad = s.validate_into::<Config>(Value::from(serde_json::json!({"name": "x"})));
349        assert!(matches!(bad, Err(IntoError::Deserialize(_))));
350        assert!(!bad.unwrap_err().to_string().is_empty());
351    }
352}