Skip to main content

polyspec_crudui_validator/
text.rs

1//! Input text (docs/spec/input-text.md).
2//!
3//! Every string and object member name of a specification, a composition file
4//! and caller data is a sequence of Unicode scalar values. A Rust `String` is
5//! always one, so the value entry points need no check. JSON text can still
6//! carry an unpaired surrogate escape such as `"\ud800"`, which `serde_json`
7//! refuses to decode. [`JsonText`] reads such a document without replacing
8//! anything, and [`check_specification`] and [`check_inputs`] report the same
9//! failures as every other runtime: the load failure `INVALID_TEXT` for a
10//! specification or a file, and `INVALID_FORM_INPUT` for other caller values.
11
12use std::collections::HashSet;
13use std::fmt;
14use std::sync::{Arc, Weak};
15
16use serde_json::{Map, Number, Value};
17
18use crate::compose::{ComposeErrorCode, ComposeLoadError};
19use crate::validate::{FormInputError, ValidateError, ValidateOptions, ValidationResult};
20use crate::{ValidateDetailOptions, ValidateListOptions};
21
22/// Message of every input text failure.
23pub const MESSAGE: &str = "Text must be Unicode scalar values";
24
25/// Message of every value beyond the limits.
26pub const VALUE_LIMIT_MESSAGE: &str = "Recursive or excessively nested value";
27
28/// The nesting depth the JSON parser accepts (same as other runtimes).
29const MAX_DEPTH: usize = 512;
30
31/// Maximum nodes a value may hold: every array, object, string, number, boolean and null.
32pub(crate) const NODE_LIMIT: usize = 1_000_000;
33
34/// Maximum levels of arrays and objects a value may nest.
35pub(crate) const NESTING_LIMIT: usize = 512;
36
37/// A string or member name of a JSON document: valid text, or the UTF-16
38/// code units of text that holds an unpaired surrogate.
39#[derive(Clone, Debug, PartialEq, Eq, Hash)]
40pub enum JsonString {
41    /// A sequence of Unicode scalar values.
42    Text(String),
43    /// Code units that include an unpaired surrogate.
44    Units(Vec<u16>),
45}
46
47impl JsonString {
48    /// The text, when it is valid.
49    pub fn as_str(&self) -> Option<&str> {
50        match self {
51            JsonString::Text(text) => Some(text),
52            JsonString::Units(_) => None,
53        }
54    }
55}
56
57/// A JSON document read without replacing any text.
58///
59/// [`JsonText::Shared`] and [`JsonText::Link`] hold what JSON text cannot write: a value placed in
60/// several parents, and a link back to a shared value that contains it, which makes a value that
61/// contains itself. Neither is a node of its own: a walk counts each visit of the value it denotes,
62/// as the TypeScript and Go runtimes do, so the node and nesting limits bound every walk. A link
63/// must refer to a value that its owner holds; the parser never produces either variant.
64#[derive(Clone, Debug)]
65pub enum JsonText {
66    /// `null`.
67    Null,
68    /// `true` or `false`.
69    Bool(bool),
70    /// A number, kept as written.
71    Number(String),
72    /// A string.
73    String(JsonString),
74    /// An array.
75    Array(Vec<JsonText>),
76    /// An object with unique member names.
77    Object(Vec<(JsonString, JsonText)>),
78    /// A value shared by several parents.
79    Shared(Arc<JsonText>),
80    /// A link to a shared value, which makes a cycle when the link is inside that value.
81    Link(Weak<JsonText>),
82}
83
84impl PartialEq for JsonText {
85    fn eq(&self, other: &Self) -> bool {
86        match (self, other) {
87            (JsonText::Null, JsonText::Null) => true,
88            (JsonText::Bool(a), JsonText::Bool(b)) => a == b,
89            (JsonText::Number(a), JsonText::Number(b)) => a == b,
90            (JsonText::String(a), JsonText::String(b)) => a == b,
91            (JsonText::Array(a), JsonText::Array(b)) => a == b,
92            (JsonText::Object(a), JsonText::Object(b)) => a == b,
93            (JsonText::Shared(a), JsonText::Shared(b)) => Arc::ptr_eq(a, b) || a == b,
94            (JsonText::Link(a), JsonText::Link(b)) => Weak::ptr_eq(a, b),
95            _ => false,
96        }
97    }
98}
99
100/// JSON text that is not one JSON document.
101#[derive(Clone, Debug, PartialEq, Eq)]
102pub struct JsonTextError {
103    /// Byte offset of the first character that is not accepted.
104    pub offset: usize,
105}
106
107impl fmt::Display for JsonTextError {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        write!(f, "Invalid JSON text at byte {}", self.offset)
110    }
111}
112
113impl std::error::Error for JsonTextError {}
114
115struct Parser<'a> {
116    bytes: &'a [u8],
117    pos: usize,
118}
119
120impl Parser<'_> {
121    fn error<T>(&self) -> Result<T, JsonTextError> {
122        Err(JsonTextError { offset: self.pos })
123    }
124
125    fn space(&mut self) {
126        while matches!(self.bytes.get(self.pos), Some(b' ' | b'\t' | b'\n' | b'\r')) {
127            self.pos += 1;
128        }
129    }
130
131    fn literal(&mut self, word: &[u8], value: JsonText) -> Result<JsonText, JsonTextError> {
132        if self.bytes[self.pos..].starts_with(word) {
133            self.pos += word.len();
134            Ok(value)
135        } else {
136            self.error()
137        }
138    }
139
140    fn value(&mut self, depth: usize) -> Result<JsonText, JsonTextError> {
141        self.space();
142        match self.bytes.get(self.pos) {
143            Some(b'{') => {
144                if depth >= MAX_DEPTH {
145                    return self.error();
146                }
147                self.pos += 1;
148                let mut members: Vec<(JsonString, JsonText)> = Vec::new();
149                let mut names: HashSet<JsonString> = HashSet::new();
150                self.space();
151                if self.bytes.get(self.pos) == Some(&b'}') {
152                    self.pos += 1;
153                    return Ok(JsonText::Object(members));
154                }
155                loop {
156                    self.space();
157                    if self.bytes.get(self.pos) != Some(&b'"') {
158                        return self.error();
159                    }
160                    let offset = self.pos;
161                    let name = self.string()?;
162                    if !names.insert(name.clone()) {
163                        return Err(JsonTextError { offset });
164                    }
165                    self.space();
166                    if self.bytes.get(self.pos) != Some(&b':') {
167                        return self.error();
168                    }
169                    self.pos += 1;
170                    let value = self.value(depth + 1)?;
171                    members.push((name, value));
172                    self.space();
173                    match self.bytes.get(self.pos) {
174                        Some(b',') => self.pos += 1,
175                        Some(b'}') => {
176                            self.pos += 1;
177                            return Ok(JsonText::Object(members));
178                        }
179                        _ => return self.error(),
180                    }
181                }
182            }
183            Some(b'[') => {
184                if depth >= MAX_DEPTH {
185                    return self.error();
186                }
187                self.pos += 1;
188                let mut items = Vec::new();
189                self.space();
190                if self.bytes.get(self.pos) == Some(&b']') {
191                    self.pos += 1;
192                    return Ok(JsonText::Array(items));
193                }
194                loop {
195                    items.push(self.value(depth + 1)?);
196                    self.space();
197                    match self.bytes.get(self.pos) {
198                        Some(b',') => self.pos += 1,
199                        Some(b']') => {
200                            self.pos += 1;
201                            return Ok(JsonText::Array(items));
202                        }
203                        _ => return self.error(),
204                    }
205                }
206            }
207            Some(b'"') => Ok(JsonText::String(self.string()?)),
208            Some(b't') => self.literal(b"true", JsonText::Bool(true)),
209            Some(b'f') => self.literal(b"false", JsonText::Bool(false)),
210            Some(b'n') => self.literal(b"null", JsonText::Null),
211            Some(b'-' | b'0'..=b'9') => self.number(),
212            _ => self.error(),
213        }
214    }
215
216    fn digits(&mut self) -> usize {
217        let start = self.pos;
218        while matches!(self.bytes.get(self.pos), Some(b'0'..=b'9')) {
219            self.pos += 1;
220        }
221        self.pos - start
222    }
223
224    fn number(&mut self) -> Result<JsonText, JsonTextError> {
225        let start = self.pos;
226        if self.bytes[self.pos] == b'-' {
227            self.pos += 1;
228        }
229        match self.bytes.get(self.pos) {
230            Some(b'0') => self.pos += 1,
231            Some(b'1'..=b'9') => {
232                self.digits();
233            }
234            _ => return self.error(),
235        }
236        if self.bytes.get(self.pos) == Some(&b'.') {
237            self.pos += 1;
238            if self.digits() == 0 {
239                return self.error();
240            }
241        }
242        if matches!(self.bytes.get(self.pos), Some(b'e' | b'E')) {
243            self.pos += 1;
244            if matches!(self.bytes.get(self.pos), Some(b'+' | b'-')) {
245                self.pos += 1;
246            }
247            if self.digits() == 0 {
248                return self.error();
249            }
250        }
251        let text = std::str::from_utf8(&self.bytes[start..self.pos]).expect("ASCII digits");
252        Ok(JsonText::Number(text.to_owned()))
253    }
254
255    fn hex(&mut self) -> Result<u16, JsonTextError> {
256        let digits = self
257            .bytes
258            .get(self.pos..self.pos + 4)
259            .ok_or(JsonTextError { offset: self.pos })?;
260        if !digits.iter().all(u8::is_ascii_hexdigit) {
261            return self.error();
262        }
263        // Hex digits are ASCII, so the four bytes are valid UTF-8.
264        let text = std::str::from_utf8(digits).expect("ASCII hex digits are UTF-8");
265        self.pos += 4;
266        Ok(u16::from_str_radix(text, 16).expect("hex digits"))
267    }
268
269    fn string(&mut self) -> Result<JsonString, JsonTextError> {
270        self.pos += 1;
271        let mut units: Vec<u16> = Vec::new();
272        loop {
273            let Some(&byte) = self.bytes.get(self.pos) else {
274                return self.error();
275            };
276            match byte {
277                b'"' => {
278                    self.pos += 1;
279                    return Ok(match String::from_utf16(&units) {
280                        Ok(text) => JsonString::Text(text),
281                        Err(_) => JsonString::Units(units),
282                    });
283                }
284                b'\\' => {
285                    self.pos += 1;
286                    let unit = match self.bytes.get(self.pos) {
287                        Some(b'"') => u16::from(b'"'),
288                        Some(b'\\') => u16::from(b'\\'),
289                        Some(b'/') => u16::from(b'/'),
290                        Some(b'b') => 0x08,
291                        Some(b'f') => 0x0c,
292                        Some(b'n') => u16::from(b'\n'),
293                        Some(b'r') => u16::from(b'\r'),
294                        Some(b't') => u16::from(b'\t'),
295                        Some(b'u') => {
296                            self.pos += 1;
297                            units.push(self.hex()?);
298                            continue;
299                        }
300                        _ => return self.error(),
301                    };
302                    self.pos += 1;
303                    units.push(unit);
304                }
305                0x00..=0x1f => return self.error(),
306                _ => {
307                    // The input is a `str`, so each character is whole.
308                    let length = match byte {
309                        0x00..=0x7f => 1,
310                        0xc0..=0xdf => 2,
311                        0xe0..=0xef => 3,
312                        _ => 4,
313                    };
314                    let character = std::str::from_utf8(&self.bytes[self.pos..self.pos + length])
315                        .expect("the input is a str, so each character is whole")
316                        .chars()
317                        .next()
318                        .ok_or(JsonTextError { offset: self.pos })?;
319                    let mut buffer = [0u16; 2];
320                    units.extend_from_slice(character.encode_utf16(&mut buffer));
321                    self.pos += character.len_utf8();
322                }
323            }
324        }
325    }
326}
327
328impl JsonText {
329    /// Read one JSON document with the grammar and nesting limit `serde_json`
330    /// applies, keeping every string as written.
331    pub fn parse(text: &str) -> Result<JsonText, JsonTextError> {
332        let mut parser = Parser {
333            bytes: text.as_bytes(),
334            pos: 0,
335        };
336        let value = parser.value(0)?;
337        parser.space();
338        if parser.pos != parser.bytes.len() {
339            return parser.error();
340        }
341        Ok(value)
342    }
343
344    /// The member `name` of an object. A shared object is read through its target; a link is not.
345    pub fn get(&self, name: &str) -> Option<&JsonText> {
346        match self {
347            JsonText::Object(members) => members
348                .iter()
349                .find(|(member, _)| member.as_str() == Some(name))
350                .map(|(_, value)| value),
351            JsonText::Shared(target) => target.get(name),
352            _ => None,
353        }
354    }
355
356    /// The item at `index` of an array. A shared array is read through its target; a link is not.
357    pub fn item(&self, index: usize) -> Option<&JsonText> {
358        match self {
359            JsonText::Array(items) => items.get(index),
360            JsonText::Shared(target) => target.item(index),
361            _ => None,
362        }
363    }
364
365    /// The value as [`Value`], or `None` when it holds invalid text, a number
366    /// `serde_json` does not accept, or a value beyond its limits.
367    pub fn to_value(&self) -> Option<Value> {
368        if value_failure(self) == Some(ValueFailure::Limit) {
369            return None;
370        }
371        json_text_to_value(self)
372    }
373
374    /// The document's shape for request checks that read only kinds and valid
375    /// names: invalid text becomes an empty string, an invalid member name a
376    /// name no valid request member uses, and a number `serde_json` does not
377    /// accept `null`. A value beyond its limits has no shape and is `null`. The
378    /// shape is never an operation input; the text checks run before any part of
379    /// it reaches an operation.
380    pub fn shape(&self) -> Value {
381        if value_failure(self) == Some(ValueFailure::Limit) {
382            return Value::Null;
383        }
384        shape_of(self)
385    }
386
387    /// Whether the first failure of the value is invalid text (see [`JsonText::invalid_path`]).
388    pub fn has_invalid_text(&self) -> bool {
389        matches!(value_failure(self), Some(ValueFailure::Text(_)))
390    }
391
392    /// The path of the first failure when it is invalid text: the path of a string, or of the
393    /// object whose member name is invalid. Members are visited in code point order of their names
394    /// after every member name is checked, and array items in index order.
395    pub fn invalid_path(&self) -> Option<Vec<String>> {
396        match value_failure(self) {
397            Some(ValueFailure::Text(path)) => Some(path),
398            _ => None,
399        }
400    }
401
402    /// Whether this value holds a limit violation (too many nodes or too deep nesting).
403    pub fn has_limit_violation(&self) -> bool {
404        value_failure(self) == Some(ValueFailure::Limit)
405    }
406}
407
408/// The first failure of a value, which the text and limit checks report.
409#[derive(Clone, Debug, PartialEq, Eq)]
410pub(crate) enum ValueFailure {
411    /// Invalid text at this path (the path of its string, or of the object whose member name is invalid).
412    Text(Vec<String>),
413    /// A value beyond its limits, reached before any text failure in the walk order.
414    Limit,
415}
416
417/// The nodes of one walk, counted per visit: a shared value visited twice counts twice.
418#[derive(Default)]
419struct Walk {
420    nodes: usize,
421}
422
423impl Walk {
424    /// Count a node at `depth`; false when it takes the value beyond its limits.
425    fn admit(&mut self, node: &JsonText, depth: usize) -> bool {
426        self.nodes += 1;
427        if self.nodes > NODE_LIMIT {
428            return false;
429        }
430        depth < NESTING_LIMIT || !matches!(node, JsonText::Array(_) | JsonText::Object(_))
431    }
432}
433
434/// Run `f` on the value that `value` denotes: a shared or linked value is its target, which is no
435/// node of its own. A link is followed by upgrading it; its owner holds the target while it is read.
436fn with_node<R>(value: &JsonText, f: impl FnOnce(&JsonText) -> R) -> R {
437    match value {
438        JsonText::Shared(target) => with_node(target, f),
439        JsonText::Link(target) => {
440            let target = target
441                .upgrade()
442                .expect("a link refers to a shared value that its owner holds");
443            with_node(&target, f)
444        }
445        other => f(other),
446    }
447}
448
449/// Whether the value holds a failure, walked in member order: the quick walk that precedes the
450/// ordered one, with its own node count.
451fn holds_failure(value: &JsonText, depth: usize, walk: &mut Walk) -> bool {
452    with_node(value, |node| {
453        if !walk.admit(node, depth) {
454            return true;
455        }
456        match node {
457            JsonText::String(text) => text.as_str().is_none(),
458            JsonText::Array(items) => items
459                .iter()
460                .any(|item| holds_failure(item, depth + 1, walk)),
461            JsonText::Object(members) => members.iter().any(|(name, member)| {
462                name.as_str().is_none() || holds_failure(member, depth + 1, walk)
463            }),
464            _ => false,
465        }
466    })
467}
468
469/// The first failure in walk order: array items in index order; for an object, every member name
470/// first, then the members in code point order of their names.
471fn first_failure(
472    value: &JsonText,
473    path: &mut Vec<String>,
474    depth: usize,
475    walk: &mut Walk,
476) -> Option<ValueFailure> {
477    with_node(value, |node| {
478        if !walk.admit(node, depth) {
479            return Some(ValueFailure::Limit);
480        }
481        match node {
482            JsonText::String(text) => text
483                .as_str()
484                .is_none()
485                .then(|| ValueFailure::Text(path.clone())),
486            JsonText::Array(items) => {
487                for (index, item) in items.iter().enumerate() {
488                    path.push(index.to_string());
489                    let found = first_failure(item, path, depth + 1, walk);
490                    path.pop();
491                    if found.is_some() {
492                        return found;
493                    }
494                }
495                None
496            }
497            JsonText::Object(members) => {
498                let mut named = Vec::with_capacity(members.len());
499                for (name, member) in members {
500                    match name.as_str() {
501                        Some(text) => named.push((text, member)),
502                        None => return Some(ValueFailure::Text(path.clone())),
503                    }
504                }
505                // Byte order of UTF-8 is code point order.
506                named.sort_by(|a, b| a.0.cmp(b.0));
507                for (name, member) in named {
508                    path.push(name.to_owned());
509                    let found = first_failure(member, path, depth + 1, walk);
510                    path.pop();
511                    if found.is_some() {
512                        return found;
513                    }
514                }
515                None
516            }
517            _ => None,
518        }
519    })
520}
521
522/// The first failure of `value`: the quick walk finds whether there is one, and the ordered walk
523/// reports it. Each walk counts its own nodes, as the TypeScript runtime does.
524fn value_failure(value: &JsonText) -> Option<ValueFailure> {
525    if !holds_failure(value, 0, &mut Walk::default()) {
526        return None;
527    }
528    Some(
529        first_failure(value, &mut Vec::new(), 0, &mut Walk::default())
530            .expect("the ordered walk reports the failure that the quick walk found"),
531    )
532}
533
534/// The shape of a value within its limits, as [`JsonText::shape`] states it.
535fn shape_of(value: &JsonText) -> Value {
536    with_node(value, |node| match node {
537        JsonText::Array(items) => Value::Array(items.iter().map(shape_of).collect()),
538        JsonText::Object(members) => {
539            let mut map = Map::new();
540            for (index, (name, member)) in members.iter().enumerate() {
541                let name = match name {
542                    JsonString::Text(text) => text.clone(),
543                    JsonString::Units(_) => format!("\u{FFFF}{index}"),
544                };
545                map.insert(name, shape_of(member));
546            }
547            Value::Object(map)
548        }
549        JsonText::String(JsonString::Units(_)) => Value::String(String::new()),
550        JsonText::String(JsonString::Text(text)) => Value::String(text.clone()),
551        JsonText::Null => Value::Null,
552        JsonText::Bool(value) => Value::Bool(*value),
553        JsonText::Number(text) => number_value(text).unwrap_or(Value::Null),
554        JsonText::Shared(_) | JsonText::Link(_) => unreachable!("with_node resolves shared values"),
555    })
556}
557
558/// Check a specification and then the composition files an operation reads.
559/// Invalid text is the `INVALID_TEXT` load failure located at its
560/// specification path, or at the file name followed by its path in the file;
561/// an invalid file name is located at the empty path. A value beyond its limits
562/// is `INVALID_FORM_INPUT` naming `spec` or `files`. Each value is checked in
563/// walk order: the first failure of the value decides its error.
564pub fn check_specification(
565    spec: Option<&JsonText>,
566    files: Option<&JsonText>,
567) -> Result<(), ValidateError> {
568    for (name, value) in [("spec", spec), ("files", files)] {
569        if let Some(value) = value {
570            match value_failure(value) {
571                Some(ValueFailure::Limit) => {
572                    return Err(ValidateError::Input(FormInputError::new(format!(
573                        "{VALUE_LIMIT_MESSAGE}: {name}"
574                    ))));
575                }
576                Some(ValueFailure::Text(trace)) => {
577                    return Err(ValidateError::Load(ComposeLoadError::with_trace(
578                        ComposeErrorCode::InvalidText,
579                        MESSAGE,
580                        trace,
581                    )));
582                }
583                None => {}
584            }
585        }
586    }
587    Ok(())
588}
589
590/// Check named caller values in order; absent values are skipped. Invalid text
591/// is `INVALID_FORM_INPUT` naming the value and its path, and a value beyond its limits
592/// is `INVALID_FORM_INPUT` naming the value. Each value's first failure decides its error.
593pub fn check_inputs(inputs: &[(&str, Option<&JsonText>)]) -> Result<(), FormInputError> {
594    for (name, value) in inputs {
595        if let Some(value) = value {
596            match value_failure(value) {
597                Some(ValueFailure::Limit) => {
598                    return Err(FormInputError::new(format!(
599                        "{VALUE_LIMIT_MESSAGE}: {name}"
600                    )));
601                }
602                Some(ValueFailure::Text(path)) => {
603                    let mut location = vec![(*name).to_owned()];
604                    location.extend(path);
605                    return Err(FormInputError::new(format!(
606                        "{MESSAGE}: {}",
607                        location.join(".")
608                    )));
609                }
610                None => {}
611            }
612        }
613    }
614    Ok(())
615}
616
617/// The value of a JSON number. A number that `serde_json` does not accept (out of its
618/// range) is invalid input: the callers return `None`, and the text checks report it.
619#[allow(clippy::disallowed_methods, reason = "a number serde_json rejects is invalid")]
620fn number_value(text: &str) -> Option<Value> {
621    serde_json::from_str::<Number>(text).ok().map(Value::Number)
622}
623
624/// Convert a value to serde_json::Value. Invalid text becomes None, as do numbers serde_json does
625/// not accept. The callers convert only values within their limits, so the nesting is bounded by
626/// the nesting limit and the recursion stays within it.
627fn json_text_to_value(value: &JsonText) -> Option<Value> {
628    with_node(value, |node| match node {
629        JsonText::Null => Some(Value::Null),
630        JsonText::Bool(b) => Some(Value::Bool(*b)),
631        JsonText::Number(text) => number_value(text),
632        JsonText::String(text) => Some(Value::String(text.as_str()?.to_owned())),
633        JsonText::Array(items) => items
634            .iter()
635            .map(json_text_to_value)
636            .collect::<Option<Vec<_>>>()
637            .map(Value::Array),
638        JsonText::Object(members) => {
639            // Member names are checked before any value is converted.
640            let mut names = Vec::with_capacity(members.len());
641            for (name, _) in members {
642                names.push(name.as_str()?);
643            }
644            let mut object = Map::new();
645            for (name, (_, member)) in names.into_iter().zip(members) {
646                object.insert(name.to_owned(), json_text_to_value(member)?);
647            }
648            Some(Value::Object(object))
649        }
650        JsonText::Shared(_) | JsonText::Link(_) => unreachable!("with_node resolves shared values"),
651    })
652}
653
654/// A JSON text member that is not text, or a number `serde_json` does not
655/// accept, after the text checks passed.
656fn decoded(value: Option<&JsonText>) -> Result<Option<Value>, ValidateError> {
657    value
658        .map(|value| {
659            json_text_to_value(value).ok_or_else(|| {
660                ValidateError::Input(FormInputError::new("Numbers must be finite JSON numbers"))
661            })
662        })
663        .transpose()
664}
665
666/// The decoded files and base path of a JSON text validation.
667type Composition = (Option<Map<String, Value>>, Option<String>);
668
669/// Decode the files and base path of a JSON text validation.
670fn composition(
671    files: Option<&JsonText>,
672    basepath: Option<&JsonText>,
673) -> Result<Composition, ValidateError> {
674    let files = decoded(files)?.and_then(|files| match files {
675        Value::Object(files) => Some(files),
676        _ => None,
677    });
678    let basepath = decoded(basepath)?.and_then(|basepath| basepath.as_str().map(str::to_owned));
679    Ok((files, basepath))
680}
681
682/// [`validate`](crate::validate()) over JSON text. The specification, the files,
683/// the data and the base path are checked for invalid text in that order; each
684/// argument is then decoded and validated. Absent data validates an empty
685/// object.
686pub fn validate_text(
687    spec: &JsonText,
688    data: Option<&JsonText>,
689    files: Option<&JsonText>,
690    basepath: Option<&JsonText>,
691) -> Result<ValidationResult, ValidateError> {
692    check_specification(Some(spec), files)?;
693    check_inputs(&[("data", data), ("options.basepath", basepath)])?;
694    let (files, basepath) = composition(files, basepath)?;
695    let spec = decoded(Some(spec))?.unwrap_or(Value::Null);
696    let data = decoded(data)?.unwrap_or_else(|| Value::Object(Map::new()));
697    crate::validate(
698        &spec,
699        &data,
700        &ValidateOptions {
701            files,
702            loader: None,
703            basepath,
704        },
705    )
706}
707
708/// The input of a list or detail specification after the checks of [`validate_text`] for their
709/// text, which `validate` then validates: the decoded specification, the files and the base path.
710fn structure_text(
711    spec: &JsonText,
712    files: Option<&JsonText>,
713    basepath: Option<&JsonText>,
714    validate: impl FnOnce(&Value, Option<Map<String, Value>>, Option<String>) -> Result<(), ValidateError>,
715) -> Result<(), ValidateError> {
716    check_specification(Some(spec), files)?;
717    check_inputs(&[("options.basepath", basepath)])?;
718    let (files, basepath) = composition(files, basepath)?;
719    let spec = decoded(Some(spec))?.unwrap_or(Value::Null);
720    validate(&spec, files, basepath)
721}
722
723/// Defines a function that validates a list or a detail specification given as JSON text, with the checks of
724/// [`validate_text`] for the specification, the files and the base path.
725macro_rules! structure_text_validator {
726    ($(#[$meta:meta])* $name:ident, $options:ident, $validate:ident) => {
727        $(#[$meta])*
728        pub fn $name(
729            spec: &JsonText,
730            files: Option<&JsonText>,
731            basepath: Option<&JsonText>,
732        ) -> Result<(), ValidateError> {
733            structure_text(spec, files, basepath, |spec, files, basepath| {
734                let options = $options { files, loader: None, basepath };
735                Ok(crate::$validate(spec, &options)?)
736            })
737        }
738    };
739}
740
741structure_text_validator! {
742    /// [`validate_list`](crate::validate_list()) over JSON text, with the checks of
743    /// [`validate_text`] for the specification, the files and the base path.
744    validate_list_text, ValidateListOptions, validate_list
745}
746
747structure_text_validator! {
748    /// [`validate_detail`](crate::validate_detail()) over JSON text, with the checks
749    /// of [`validate_text`] for the specification, the files and the base path.
750    validate_detail_text, ValidateDetailOptions, validate_detail
751}
752
753#[cfg(test)]
754mod tests {
755    use super::*;
756
757    /// An object of `count` distinct members.
758    fn object(count: usize) -> String {
759        let members: Vec<String> = (0..count).map(|i| format!("\"m{i}\":{i}")).collect();
760        format!("{{{}}}", members.join(","))
761    }
762
763    #[test]
764    fn repeated_member_is_rejected() {
765        for source in [
766            r#"{"a":1,"b":2,"a":3}"#,
767            r#"{"a":{"b":1,"\u0062":2}}"#,
768            r#"[{"a":1,"a":2}]"#,
769        ] {
770            assert!(JsonText::parse(source).is_err(), "{source}");
771        }
772    }
773
774    #[test]
775    fn decoded_object_values_keep_their_names_and_order() {
776        let source = r#"{"a":1,"b":{"x":2,"y":3},"c":[4,{"z":5}]}"#;
777        let parsed = JsonText::parse(source).unwrap();
778        let decoded = super::json_text_to_value(&parsed).unwrap();
779        assert_eq!(serde_json::to_string(&decoded).unwrap(), source);
780    }
781
782    /// An object of 100,000 distinct members, each found in constant time, is read within a
783    /// second; comparing each member with every earlier one makes 5 * 10^9 comparisons and does
784    /// not end before the per-test timeout of the run-tests tool of @polyspec/kit.
785    #[test]
786    fn reading_many_members_ends() {
787        JsonText::parse(&object(100_000)).unwrap();
788        assert!(JsonText::parse(&format!("{{\"m0\":0,{}", &object(100_000)[1..])).is_err());
789    }
790}