Skip to main content

corvus_json_schema/
options.rs

1//! Compile options and errors (the counterparts of `JsonSchemaEvaluatorOptions` and the evaluator exceptions).
2
3use std::collections::HashMap;
4use std::fmt;
5use std::sync::Arc;
6use std::time::Duration;
7
8use serde_json::Value;
9
10use crate::dialect::Dialect;
11
12/// Resolves a schema document by absolute URI; returns `None` if it is unknown.
13pub type DocumentResolver = Arc<dyn Fn(&str) -> Option<Value> + Send + Sync>;
14
15/// A custom format assertion over the string value (or the number's JSON text, for numbers).
16pub type FormatValidator = Arc<dyn Fn(&str) -> bool + Send + Sync>;
17
18/// Options for compiling a schema.
19#[derive(Clone)]
20pub struct CompileOptions {
21    /// The dialect for documents without `$schema`. Defaults to 2020-12.
22    pub default_dialect: Dialect,
23    /// Whether `format` is asserted. `None` (the default) follows the vocabularies: the 2020-12 `format-assertion`
24    /// vocabulary asserts, everything else annotates.
25    pub assert_format: Option<bool>,
26    /// When `assert_format` is `None`, assert `format` in draft 4 to 7 too.
27    pub assert_format_in_legacy_drafts: bool,
28    /// Assert `contentEncoding`/`contentMediaType` in draft 7 (the only draft that asserts them). Defaults to true.
29    pub assert_content: bool,
30    /// Custom format assertions, by format name; they take precedence over the built-in set.
31    pub formats: HashMap<String, FormatValidator>,
32    /// Resolves remote documents. The standard metaschemas are always available.
33    pub resolve_document: Option<DocumentResolver>,
34    /// The base URI of the root document.
35    pub base_uri: Option<String>,
36    /// A reference (relative to the root) to evaluate from, e.g. `#/$defs/item`. Defaults to the root.
37    pub entry_point: Option<String>,
38    /// Maximum depth of in-place recursion on a cycle before evaluation is abandoned. Defaults to 128.
39    pub max_depth: u32,
40    /// The time one match of a `pattern` or `patternProperties` regular expression against one string may take.
41    /// Defaults to [`DEFAULT_PATTERN_MATCH_LIMIT`], one second. A match that takes longer is abandoned, and the
42    /// validation it belongs to reports a [`PatternMatchLimitError`]. `None`, or a limit of zero, turns the limit
43    /// off, for a caller who trusts both the schema and the instances. Without the limit a match also has no limit
44    /// on its memory.
45    ///
46    /// The limit is on each match, not on the validation. Only the backtracking matcher can take long, and it
47    /// reads the clock every 4,096 steps, so a match is abandoned within tens of microseconds of the limit. The
48    /// matchers that take time proportional to the string are not stopped: a string is given to one only when it
49    /// can answer within the limit.
50    pub pattern_match_limit: Option<Duration>,
51}
52
53/// The time one match of a `pattern` or `patternProperties` regular expression against one string may take, unless
54/// [`CompileOptions::pattern_match_limit`] says otherwise.
55pub const DEFAULT_PATTERN_MATCH_LIMIT: Duration = Duration::from_secs(1);
56
57impl Default for CompileOptions {
58    fn default() -> Self {
59        CompileOptions {
60            default_dialect: Dialect::Draft202012,
61            assert_format: None,
62            assert_format_in_legacy_drafts: false,
63            assert_content: true,
64            formats: HashMap::new(),
65            resolve_document: None,
66            base_uri: None,
67            entry_point: None,
68            max_depth: 128,
69            pattern_match_limit: Some(DEFAULT_PATTERN_MATCH_LIMIT),
70        }
71    }
72}
73
74impl fmt::Debug for CompileOptions {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        f.debug_struct("CompileOptions")
77            .field("default_dialect", &self.default_dialect)
78            .field("assert_format", &self.assert_format)
79            .field("assert_format_in_legacy_drafts", &self.assert_format_in_legacy_drafts)
80            .field("assert_content", &self.assert_content)
81            .field("formats", &self.formats.keys().collect::<Vec<_>>())
82            .field("resolve_document", &self.resolve_document.is_some())
83            .field("base_uri", &self.base_uri)
84            .field("entry_point", &self.entry_point)
85            .field("max_depth", &self.max_depth)
86            .field("pattern_match_limit", &self.pattern_match_limit)
87            .finish()
88    }
89}
90
91/// A schema could not be compiled (an unresolvable reference, an invalid pattern, a pattern over a limit of size).
92/// [`SchemaCompilationError::kind`] tells the errors of a regular expression apart.
93#[derive(Clone, Debug, PartialEq, Eq)]
94pub struct SchemaCompilationError {
95    message: String,
96    kind: SchemaCompilationErrorKind,
97}
98
99/// What kind of error a [`SchemaCompilationError`] is.
100#[derive(Clone, Debug, PartialEq, Eq)]
101#[non_exhaustive]
102pub enum SchemaCompilationErrorKind {
103    /// Any error that is not about a regular expression: an unresolvable reference, for one.
104    Other,
105    /// A `pattern`, or a key of `patternProperties`, is not a valid ECMA-262 regular expression.
106    InvalidPattern,
107    /// A regular expression is over a limit of size. It is not the error of one that is not valid: the pattern was
108    /// not read to its end, so nothing is known of whether it is valid.
109    PatternTooLarge(PatternTooLargeError),
110    /// Matching a regular expression against a property name the schema declares, which is done once when the
111    /// schema is compiled, reached the limit of a match (see [`CompileOptions::pattern_match_limit`]).
112    PatternMatchLimit(PatternMatchLimitError),
113}
114
115impl SchemaCompilationError {
116    pub(crate) fn new(message: impl Into<String>) -> Self {
117        SchemaCompilationError { message: message.into(), kind: SchemaCompilationErrorKind::Other }
118    }
119
120    pub(crate) fn of_kind(message: impl Into<String>, kind: SchemaCompilationErrorKind) -> Self {
121        SchemaCompilationError { message: message.into(), kind }
122    }
123
124    /// The reason compilation failed.
125    pub fn message(&self) -> &str {
126        &self.message
127    }
128
129    /// The kind of error, which tells a regular expression that is too large from one that is not valid.
130    pub fn kind(&self) -> &SchemaCompilationErrorKind {
131        &self.kind
132    }
133}
134
135impl fmt::Display for SchemaCompilationError {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        f.write_str(&self.message)
138    }
139}
140
141impl std::error::Error for SchemaCompilationError {}
142
143/// A regular expression of a schema is over a limit of size, so the schema was not compiled.
144#[derive(Clone, Debug, PartialEq, Eq)]
145pub struct PatternTooLargeError {
146    pattern: String,
147    keyword: &'static str,
148    limit: &'static str,
149    value: usize,
150    reason: String,
151}
152
153impl PatternTooLargeError {
154    pub(crate) fn new(pattern: &str, keyword: &'static str, limit: &'static str, value: usize, reason: String) -> Self {
155        PatternTooLargeError { pattern: pattern.to_string(), keyword, limit, value, reason }
156    }
157
158    /// The regular expression.
159    pub fn pattern(&self) -> &str {
160        &self.pattern
161    }
162
163    /// The keyword that holds it, `pattern` or `patternProperties`.
164    pub fn keyword(&self) -> &str {
165        self.keyword
166    }
167
168    /// The name of the limit: `length` (the bytes of the pattern, 1,048,576 at most), `nesting` (the depth of its
169    /// groups, 1,000 at most) or `size` (the bytes counted for it as it is read, 1,048,576 at most).
170    pub fn limit(&self) -> &str {
171        self.limit
172    }
173
174    /// The value of that limit.
175    pub fn value(&self) -> usize {
176        self.value
177    }
178}
179
180impl fmt::Display for PatternTooLargeError {
181    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182        write!(
183            f,
184            "The regular expression {} in {} is too large to read as an ECMA-262 regular expression: {}.",
185            crate::pattern::shown_pattern(&self.pattern),
186            self.keyword,
187            self.reason
188        )
189    }
190}
191
192impl std::error::Error for PatternTooLargeError {}
193
194/// A validation was abandoned because one match of a `pattern` or `patternProperties` regular expression reached
195/// its limit. The instance is then neither valid nor not valid.
196#[derive(Clone, Debug, PartialEq, Eq)]
197pub struct PatternMatchLimitError {
198    pattern: String,
199    limit: Duration,
200    memory: bool,
201}
202
203impl PatternMatchLimitError {
204    pub(crate) fn new(pattern: &str, limit: Duration, memory: bool) -> Self {
205        PatternMatchLimitError { pattern: pattern.to_string(), limit, memory }
206    }
207
208    /// The regular expression.
209    pub fn pattern(&self) -> &str {
210        &self.pattern
211    }
212
213    /// The time the match was allowed (see [`CompileOptions::pattern_match_limit`]).
214    pub fn limit(&self) -> Duration {
215        self.limit
216    }
217
218    /// Whether the match was abandoned for the memory of its backtracking, not for its time: it held 1,048,576
219    /// choices, which is 24 MB.
220    pub fn is_memory(&self) -> bool {
221        self.memory
222    }
223}
224
225impl fmt::Display for PatternMatchLimitError {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        let pattern = crate::pattern::shown_pattern(&self.pattern);
228        if self.memory {
229            write!(
230                f,
231                "A match of the regular expression {pattern} was abandoned: it needed more than the limit of \
232                 1,048,576 backtracking entries."
233            )
234        } else {
235            write!(
236                f,
237                "A match of the regular expression {pattern} was abandoned: it took longer than the limit of {:?}.",
238                self.limit
239            )
240        }
241    }
242}
243
244impl std::error::Error for PatternMatchLimitError {}
245
246/// The validation was abandoned at a limit, and gave no answer. It is the error of the functions whose error type
247/// was there before the limit of a match was (`validate`, `validate_instance`, `evaluate`, `evaluate_instance`, and
248/// inside [`crate::JsonValidationError::DepthExceeded`] `validate_json` and `evaluate_json`).
249///
250/// The name is from when there was one limit: an evaluation that recursed in place beyond `max_depth` (a schema
251/// that loops without consuming the instance). A match of a regular expression that reached its limit (see
252/// [`CompileOptions::pattern_match_limit`]) is reported as this error too. The type has one value, so it does not
253/// say which limit it was, and its message names both. The `try_` functions (`Validator::try_validate` and the
254/// rest) return a [`SchemaEvaluationError`], which says which.
255#[derive(Clone, Copy, Debug, PartialEq, Eq)]
256pub struct SchemaEvaluationDepthError;
257
258impl fmt::Display for SchemaEvaluationDepthError {
259    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260        f.write_str(
261            "The validation was abandoned at a limit: the schema recursed in place beyond the maximum depth, or a \
262             match of a regular expression reached its limit. The try_ functions say which.",
263        )
264    }
265}
266
267impl std::error::Error for SchemaEvaluationDepthError {}
268
269/// Why an evaluation gave no answer. The instance is then neither valid nor not valid.
270#[derive(Clone, Debug, PartialEq, Eq)]
271#[non_exhaustive]
272pub enum SchemaEvaluationError {
273    /// The text is not valid JSON (the functions that take JSON text).
274    InvalidJson(crate::JsonParseError),
275    /// Evaluation recursed in place beyond the maximum depth.
276    DepthExceeded(SchemaEvaluationDepthError),
277    /// A match of a regular expression reached its limit.
278    PatternMatchLimit(PatternMatchLimitError),
279}
280
281impl fmt::Display for SchemaEvaluationError {
282    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283        match self {
284            SchemaEvaluationError::InvalidJson(e) => write!(f, "invalid JSON: {e}"),
285            // Not the message of the value inside, which is the one error of the functions that cannot say which
286            // limit it was, and so names both.
287            SchemaEvaluationError::DepthExceeded(_) => {
288                f.write_str("The schema recursed in place beyond the maximum depth.")
289            }
290            SchemaEvaluationError::PatternMatchLimit(e) => e.fmt(f),
291        }
292    }
293}
294
295impl std::error::Error for SchemaEvaluationError {
296    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
297        match self {
298            SchemaEvaluationError::InvalidJson(e) => Some(e),
299            SchemaEvaluationError::DepthExceeded(e) => Some(e),
300            SchemaEvaluationError::PatternMatchLimit(e) => Some(e),
301        }
302    }
303}