Skip to main content

schema_emit_runtime/
lib.rs

1#![doc = include_str!("../README.md")]
2#![no_std]
3
4#[cfg(feature = "decode")]
5extern crate std;
6
7#[cfg(feature = "decode")]
8pub mod decode;
9mod place;
10
11pub use pattern_emit::{Pattern, Unusable};
12pub use place::{under, Trail};
13
14/// Why a value fails its check.
15#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
16pub enum Reason {
17    /// A required field holds nothing.
18    Absent,
19    /// A value past its width or bounds, outside its choices, or of a length or count past its
20    /// limits.
21    Range,
22    /// A list with two items equal by JSON Schema's rule.
23    Duplicate,
24    /// A value that isn't what its type says: text that isn't UTF-8, a list the embedder can't
25    /// read.
26    Cast,
27}
28
29impl Reason {
30    /// Its name in a failure.
31    pub const fn name(self) -> &'static str {
32        match self {
33            Reason::Absent => "absent",
34            Reason::Range => "range",
35            Reason::Duplicate => "duplicate",
36            Reason::Cast => "cast",
37        }
38    }
39}
40
41/// Where a check sends each failure.
42///
43/// `path` names the field: `<Model>.<field>`, a nested model's field after it
44/// (`Value.climate.temp_c`), `[]` for each list item, filled from `at` in order
45/// (`Value.readings[]` with `at` `[1]` is `Value.readings[1]`).
46///
47/// A model that reaches itself marks its steps in `path` as `(<step>)*`: `Value(.next)*.n`; with
48/// several, `(.left|.right)*`. In `at` the mark takes its repeat count, then per repeat the
49/// index of the step taken (only with several steps) and the indices of the `[]` inside it. The
50/// `[]` before the mark come first in `at`, those after it last. `Value(.next)*.n` with `at`
51/// `[2]` is `Value.next.next.n`. `Value.root(.children[])*.leaf[]` with `at` `[2, 4, 1, 7]` is
52/// `Value.root.children[4].children[1].leaf[7]`. `Value(.left|.right)*.n` with `at` `[2, 0, 1]`
53/// is `Value.left.right.n`. A count of 0 drops the step.
54pub trait Report {
55    fn fail(&mut self, path: &'static str, at: &[u32], reason: Reason);
56}
57
58/// One failure, as [`Failures`] keeps it: up to [`DEPTH`] list indices.
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60pub struct Failure {
61    pub path: &'static str,
62    pub at: [u32; DEPTH],
63    pub reason: Reason,
64}
65
66/// Entries of `at` a [`Failure`] keeps; deeper ones read as 0.
67pub const DEPTH: usize = 4;
68
69/// The first `N` failures, and how many more were dropped: a value's failures are capped.
70#[derive(Clone, Copy, Debug)]
71pub struct Failures<const N: usize> {
72    list: [Option<Failure>; N],
73    len: usize,
74    pub dropped: u32,
75}
76
77impl<const N: usize> Default for Failures<N> {
78    fn default() -> Self {
79        Failures {
80            list: [None; N],
81            len: 0,
82            dropped: 0,
83        }
84    }
85}
86
87impl<const N: usize> Failures<N> {
88    /// Each kept failure, in the order reported.
89    pub fn iter(&self) -> impl Iterator<Item = &Failure> {
90        self.list.iter().flatten()
91    }
92
93    pub fn is_empty(&self) -> bool {
94        self.len == 0 && self.dropped == 0
95    }
96
97    pub fn len(&self) -> usize {
98        self.len
99    }
100}
101
102impl<const N: usize> Report for Failures<N> {
103    fn fail(&mut self, path: &'static str, at: &[u32], reason: Reason) {
104        let mut kept = [0; DEPTH];
105        for (k, a) in kept.iter_mut().zip(at) {
106            *k = *a;
107        }
108        match self.list.get_mut(self.len) {
109            Some(slot) => {
110                *slot = Some(Failure {
111                    path,
112                    at: kept,
113                    reason,
114                });
115                self.len = self.len.saturating_add(1);
116            }
117            None => self.dropped = self.dropped.saturating_add(1),
118        }
119    }
120}
121
122/// Whether every check reported to it held: a branch of `anyOf`, `oneOf`, or `if` over an object
123/// or a list checks into one, so its verdict is one condition.
124#[derive(Clone, Copy, Debug, Default)]
125pub struct Holds {
126    failed: bool,
127}
128
129impl Holds {
130    pub fn held(&self) -> bool {
131        !self.failed
132    }
133}
134
135impl Report for Holds {
136    fn fail(&mut self, _: &'static str, _: &[u32], _: Reason) {
137        self.failed = true;
138    }
139}
140
141/// Keeps the first failure at or under one path, and ignores the rest.
142#[derive(Clone, Copy, Debug)]
143pub struct Under {
144    path: &'static str,
145    pub reason: Option<Reason>,
146}
147
148impl Under {
149    pub const fn new(path: &'static str) -> Under {
150        Under { path, reason: None }
151    }
152}
153
154impl Report for Under {
155    fn fail(&mut self, path: &'static str, at: &[u32], reason: Reason) {
156        if self.reason.is_none() && under(self.path, path, at) {
157            self.reason = Some(reason);
158        }
159    }
160}
161
162/// Whether `x` is a whole multiple of `m`, as JSON Schema's `multipleOf` reads it: the quotient
163/// is an integer, and a quotient past `f64`'s range is not.
164pub fn multiple_of(x: f64, m: f64) -> bool {
165    let q = x / m;
166    q.is_finite() && q == trunc(q)
167}
168
169/// `f64::trunc`, which `core` lacks: a finite value at or past 2^52 is already whole.
170fn trunc(x: f64) -> f64 {
171    const WHOLE: f64 = 4_503_599_627_370_496.0;
172    if x.is_nan() || x.abs() >= WHOLE {
173        return x;
174    }
175    // In range of i64 here: |x| < 2^52.
176    #[allow(clippy::cast_possible_truncation)]
177    let t = x as i64;
178    t as f64
179}
180
181/// Text length in Unicode scalar values, as JSON Schema counts it.
182pub fn chars(s: &str) -> u64 {
183    s.chars().fold(0u64, |n, _| n.saturating_add(1))
184}
185
186#[cfg(test)]
187mod tests;