Skip to main content

tilewright_source/
schema.rs

1// Copyright 2026 Seraphina Studios
2// SPDX-License-Identifier: MIT
3
4//! Closed field schemas (`tilewright.schema/1`) and deferred references.
5
6use crate::diagnostic::Diagnostics;
7use crate::{
8    Code, Diagnostic, FieldValue, Plane,
9    files::Tree,
10    index::{Holder, Recorder, Role, Site, Target, message},
11    read::{Reader, is_identifier},
12    uid::Claims,
13};
14use std::{
15    cell::{Cell, RefCell, RefMut},
16    collections::{BTreeMap, BTreeSet},
17    rc::Rc,
18    sync::Arc,
19};
20use tilewright_jsonc::{MAX_SAFE_INTEGER, Member, Span, Value};
21
22/// Longest string a `string` field accepts unless its schema says less.
23pub const DEFAULT_MAXIMUM_STRING: usize = 65_536;
24
25/// Deepest nesting of `list` and `object` field types. A list or object
26/// field of a schema is at level 1, a list or object inside it at level 2,
27/// and so on.
28pub const MAXIMUM_FIELD_DEPTH: usize = 8;
29
30/// Largest `min_items` or `max_items` of a `list` field: the array limit of
31/// the strict JSONC dialect, so the default `max_items` accepts any array.
32pub const MAXIMUM_LIST_ITEMS: usize = 250_000;
33
34/// A closed set of fields, in declaration order, indexed by name. A schema
35/// document has one, and so does each `object` field type.
36#[derive(Clone, Debug, PartialEq)]
37pub struct Schema {
38    /// Source path of the schema document, relative to the root. Nested
39    /// `object` types share their document's path.
40    pub path: Arc<str>,
41    fields: Vec<(String, FieldSpec)>,
42    /// Positions in `fields` by name, kept with the list.
43    index: BTreeMap<String, usize>,
44    /// Positions in `fields` of the required fields, in declaration order,
45    /// listed once so that checking an owner never walks the others.
46    required: Vec<usize>,
47}
48
49impl Schema {
50    pub(crate) const fn new(path: Arc<str>) -> Self {
51        Self {
52            path,
53            fields: Vec::new(),
54            index: BTreeMap::new(),
55            required: Vec::new(),
56        }
57    }
58
59    /// Adds a field; names come from one object's keys, so they are unique.
60    fn push(&mut self, name: String, spec: FieldSpec) {
61        let position = self.fields.len();
62        if spec.required {
63            self.required.push(position);
64        }
65        self.index.insert(name.clone(), position);
66        self.fields.push((name, spec));
67    }
68
69    /// The position and spec of field `name`.
70    fn lookup(&self, name: &str) -> Option<(usize, &FieldSpec)> {
71        let position = *self.index.get(name)?;
72        self.fields.get(position).map(|(_, spec)| (position, spec))
73    }
74
75    /// The fields in declaration order.
76    #[must_use]
77    pub fn fields(&self) -> &[(String, FieldSpec)] {
78        &self.fields
79    }
80
81    #[must_use]
82    pub fn field(&self, name: &str) -> Option<&FieldSpec> {
83        self.lookup(name).map(|(_, spec)| spec)
84    }
85}
86
87#[derive(Clone, Debug, PartialEq)]
88pub struct FieldSpec {
89    pub field_type: FieldType,
90    pub required: bool,
91}
92
93#[derive(Clone, Debug, PartialEq)]
94pub enum FieldType {
95    Boolean,
96    Integer {
97        minimum: i64,
98        maximum: i64,
99    },
100    Number {
101        minimum: f64,
102        maximum: f64,
103    },
104    /// A string of at most `maximum_length` characters, one of `values`
105    /// when that is not empty.
106    String {
107        maximum_length: usize,
108        values: BTreeSet<String>,
109    },
110    /// The identifier of a definition of type `target`.
111    Reference {
112        target: String,
113    },
114    /// Any JSON value.
115    Json,
116    /// An array of `minimum_items` to `maximum_items` values, each of type
117    /// `items`.
118    List {
119        items: Box<FieldType>,
120        minimum_items: usize,
121        maximum_items: usize,
122    },
123    /// An object closed by `fields`.
124    Object {
125        fields: Schema,
126    },
127}
128
129impl FieldType {
130    /// Calls `visit` with every reference target in this type, nested ones
131    /// included, and the dotted path of the field below this one that
132    /// holds it (`[]` stands for a list's items).
133    pub(crate) fn targets<'s>(&'s self, path: &str, visit: &mut impl FnMut(&str, &'s str)) {
134        match self {
135            Self::Reference { target } => visit(path, target),
136            Self::List { items, .. } => items.targets(&format!("{path}[]"), visit),
137            Self::Object { fields } => {
138                for (name, spec) in fields.fields() {
139                    spec.field_type.targets(&format!("{path}.{name}"), visit);
140                }
141            }
142            _ => {}
143        }
144    }
145}
146
147/// Whether `name` is a valid field name: `^[a-z][a-z0-9_]{0,63}$`.
148fn is_field_name(name: &str) -> bool {
149    let bytes = name.as_bytes();
150    !bytes.is_empty()
151        && bytes.len() <= 64
152        && bytes[0].is_ascii_lowercase()
153        && bytes
154            .iter()
155            .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || *byte == b'_')
156}
157
158/// Reads a schema document at `path`. Also returns the span of the
159/// `target` of each `reference` field type, in the order
160/// [`FieldType::targets`] visits them: declaration order, a nested one
161/// after the field that holds it.
162pub(crate) fn read_schema(
163    tree: &mut Tree<'_>,
164    path: &str,
165    diagnostics: &mut Diagnostics,
166) -> Option<(Schema, Vec<Span>)> {
167    let source = tree.document(path)?;
168    let reader = Reader::new(&source, diagnostics);
169    let root = reader.object(reader.root(), &["format", "fields"])?;
170    let format_ok = reader.format(root, "tilewright.schema/1");
171    let mut targets = Vec::new();
172    let schema = read_field_specs(
173        &reader,
174        reader.required(root, "fields")?,
175        &path.into(),
176        0,
177        &mut targets,
178    );
179    schema.filter(|_| format_ok).map(|schema| (schema, targets))
180}
181
182/// Reads a `fields` object of field specs. `depth` is the number of `list`
183/// and `object` types around it. The span of each `reference` type's
184/// `target` is added to `targets`.
185fn read_field_specs<'a>(
186    reader: &Reader<'a>,
187    value: &'a Value,
188    path: &Arc<str>,
189    depth: usize,
190    targets: &mut Vec<Span>,
191) -> Option<Schema> {
192    let members = reader.members(value)?;
193    let mut schema = Schema::new(Arc::clone(path));
194    let mut ok = true;
195    for member in members {
196        // A bad name is reported and its spec is still checked.
197        let named = is_field_name(&member.key);
198        if !named {
199            reader.report(
200                member.key_span,
201                Code::Identifier,
202                format!("\"{}\" is not a valid field name", member.key),
203            );
204            ok = false;
205        }
206        match read_spec(reader, &member.value, path, depth, true, targets).filter(|_| named) {
207            Some((field_type, required)) => schema.push(
208                member.key.clone(),
209                FieldSpec {
210                    field_type,
211                    required,
212                },
213            ),
214            None => ok = false,
215        }
216    }
217    ok.then_some(schema)
218}
219
220/// Reads one field spec: its type and whether it is required. List items
221/// are never missing, so they take no `required`: `takes_required` is
222/// `false` for them. The span of a `reference` type's `target` is added
223/// to `targets`.
224#[allow(clippy::too_many_lines)]
225fn read_spec<'a>(
226    reader: &Reader<'a>,
227    value: &'a Value,
228    path: &Arc<str>,
229    depth: usize,
230    takes_required: bool,
231    targets: &mut Vec<Span>,
232) -> Option<(FieldType, bool)> {
233    if value.as_object().is_none() {
234        reader.report(
235            value.span(),
236            Code::Type,
237            format!("expected an object, found {}", value.kind()),
238        );
239        return None;
240    }
241    let spec = value;
242    let kind = reader.required(spec, "type");
243    // Keys are checked against their type's options, or, when the type is
244    // missing or unknown, against every type's, so a key no type allows is
245    // reported either way.
246    let options: &[&str] = match kind.and_then(Value::as_str) {
247        Some("integer" | "number") => &["min", "max"],
248        Some("string") => &["max_length", "values"],
249        Some("reference") => &["target"],
250        Some("list") => &["items", "min_items", "max_items"],
251        Some("object") => &["fields"],
252        Some("boolean" | "json") => &[],
253        _ => &[
254            "min",
255            "max",
256            "max_length",
257            "values",
258            "target",
259            "items",
260            "min_items",
261            "max_items",
262            "fields",
263        ],
264    };
265    let mut allowed = vec!["type"];
266    if takes_required {
267        allowed.push("required");
268    }
269    allowed.extend(options);
270    reader.object(spec, &allowed)?;
271    // Every key is read before any result is used, so one bad key never
272    // hides a problem in another.
273    let required = match spec.get("required").filter(|_| takes_required) {
274        Some(flag) => reader.boolean(flag),
275        None => Some(false),
276    };
277    let kind = kind?;
278    // A list or object too deep is reported, and the rest of the spec is
279    // still read; the parser's nesting limit bounds the recursion.
280    let too_deep = matches!(kind.as_str(), Some("list" | "object")) && depth >= MAXIMUM_FIELD_DEPTH;
281    if too_deep {
282        reader.report(
283            kind.span(),
284            Code::Range,
285            format!("list and object types nest at most {MAXIMUM_FIELD_DEPTH} deep"),
286        );
287    }
288    let field_type = match kind.as_str() {
289        Some("boolean") => Some(FieldType::Boolean),
290        Some("json") => Some(FieldType::Json),
291        Some("integer") => {
292            let bound = |key, default| match spec.get(key) {
293                Some(value) => reader.integer(value, -MAX_SAFE_INTEGER, MAX_SAFE_INTEGER),
294                None => Some(default),
295            };
296            let (minimum, maximum) = (
297                bound("min", -MAX_SAFE_INTEGER),
298                bound("max", MAX_SAFE_INTEGER),
299            );
300            ordered(
301                reader,
302                spec,
303                minimum.zip(maximum),
304                "min is greater than max",
305            )
306            .map(|(minimum, maximum)| FieldType::Integer { minimum, maximum })
307        }
308        Some("number") => {
309            let bound = |key, default: f64| match spec.get(key) {
310                Some(value) => {
311                    let number = value.as_f64();
312                    if number.is_none() {
313                        reader.report(value.span(), Code::Type, "expected a number");
314                    }
315                    number
316                }
317                None => Some(default),
318            };
319            let (minimum, maximum) = (bound("min", f64::MIN), bound("max", f64::MAX));
320            ordered(
321                reader,
322                spec,
323                minimum.zip(maximum),
324                "min is greater than max",
325            )
326            .map(|(minimum, maximum)| FieldType::Number { minimum, maximum })
327        }
328        Some("string") => {
329            let maximum_length = match spec.get("max_length") {
330                Some(value) => reader
331                    .integer(value, 1, 1_048_576)
332                    .and_then(|length| usize::try_from(length).ok()),
333                None => Some(DEFAULT_MAXIMUM_STRING),
334            };
335            let values = match spec.get("values") {
336                Some(value) => read_values(reader, value),
337                None => Some(BTreeSet::new()),
338            };
339            maximum_length
340                .zip(values)
341                .map(|(maximum_length, values)| FieldType::String {
342                    maximum_length,
343                    values,
344                })
345        }
346        Some("reference") => reader.required(spec, "target").and_then(|value| {
347            let target = reader.identifier(value)?;
348            targets.push(value.span());
349            Some(FieldType::Reference { target })
350        }),
351        Some("list") => {
352            let count = |key, default| match spec.get(key) {
353                Some(value) => {
354                    let limit = i64::try_from(MAXIMUM_LIST_ITEMS).unwrap_or(i64::MAX);
355                    reader
356                        .integer(value, 0, limit)
357                        .and_then(|count| usize::try_from(count).ok())
358                }
359                None => Some(default),
360            };
361            let (minimum, maximum) = (
362                count("min_items", 0),
363                count("max_items", MAXIMUM_LIST_ITEMS),
364            );
365            let items = reader
366                .required(spec, "items")
367                .and_then(|items| read_spec(reader, items, path, depth + 1, false, targets));
368            let bounds = ordered(
369                reader,
370                spec,
371                minimum.zip(maximum),
372                "min_items is greater than max_items",
373            );
374            bounds.zip(items).map(
375                |((minimum_items, maximum_items), (items, _))| FieldType::List {
376                    items: Box::new(items),
377                    minimum_items,
378                    maximum_items,
379                },
380            )
381        }
382        Some("object") => reader
383            .required(spec, "fields")
384            .and_then(|fields| read_field_specs(reader, fields, path, depth + 1, targets))
385            .map(|fields| FieldType::Object { fields }),
386        _ => {
387            reader.report(
388                kind.span(),
389                Code::Range,
390                "type must be boolean, integer, number, string, reference, json, list or object",
391            );
392            None
393        }
394    };
395    field_type.zip(required).filter(|_| !too_deep)
396}
397
398/// `bounds` when its lower end is not above its upper end; otherwise reports
399/// `message` at `spec`.
400fn ordered<T: PartialOrd>(
401    reader: &Reader<'_>,
402    spec: &Value,
403    bounds: Option<(T, T)>,
404    message: &str,
405) -> Option<(T, T)> {
406    let (lower, upper) = bounds?;
407    if lower > upper {
408        reader.report(spec.span(), Code::Range, message);
409        return None;
410    }
411    Some((lower, upper))
412}
413
414/// A string spec's `values`: unique strings, every item checked.
415fn read_values<'a>(reader: &Reader<'a>, value: &'a Value) -> Option<BTreeSet<String>> {
416    let mut values = BTreeSet::new();
417    let mut ok = true;
418    for item in reader.array(value)? {
419        match reader.string(item) {
420            Some(text) if !values.insert(text.to_owned()) => {
421                reader.report(item.span(), Code::Duplicate, "value listed twice");
422                ok = false;
423            }
424            Some(_) => {}
425            None => ok = false,
426        }
427    }
428    ok.then_some(values)
429}
430
431/// What references of one load may resolve to. Every target set is known
432/// before any reference to it is read, so each reference is checked as it
433/// is read: render layers, ground layers and definition types from the
434/// profile before its schemas are read, definition keys from the `defs/`
435/// layout before any definition is parsed, a plane's zones before its
436/// zone parents and default zone are checked, planes and their anchors
437/// before any chunk is read, and terrains before any transition. It also
438/// holds the object uids claimed so far, so a duplicate is reported where
439/// it is read.
440///
441/// Every reference the loader checks or follows goes through
442/// [`References::check`] or [`References::follow`], which also record it
443/// when the load builds a [`ReferenceIndex`](crate::ReferenceIndex).
444pub(crate) struct References {
445    render_layers: BTreeSet<String>,
446    ground_layers: BTreeSet<String>,
447    definition_types: BTreeSet<String>,
448    /// Definition IDs by type, from the file names under `defs/`.
449    definitions: BTreeMap<String, BTreeSet<String>>,
450    /// Anchor IDs by plane, for every plane that loaded.
451    planes: BTreeMap<String, BTreeSet<String>>,
452    /// Zone IDs by plane, for every plane whose zones were read.
453    zones: BTreeMap<String, BTreeSet<String>>,
454    /// The terrains of the transitions document.
455    terrains: BTreeSet<String>,
456    uids: Claims,
457    /// Present while the load records its references: from its start
458    /// until its first diagnostic, after which no index is returned.
459    recorder: RefCell<Option<Recorder>>,
460    /// The diagnostics the load has kept so far, shared with every
461    /// collection of the load.
462    problems: Rc<Cell<usize>>,
463}
464
465impl References {
466    /// The references of a load; `record` makes it keep them until the
467    /// load's diagnostics, counted by `problems`, are no longer none.
468    pub fn new(record: bool, problems: Rc<Cell<usize>>) -> Self {
469        Self {
470            render_layers: BTreeSet::new(),
471            ground_layers: BTreeSet::new(),
472            definition_types: BTreeSet::new(),
473            definitions: BTreeMap::new(),
474            planes: BTreeMap::new(),
475            zones: BTreeMap::new(),
476            terrains: BTreeSet::new(),
477            uids: Claims::default(),
478            recorder: RefCell::new(record.then(Recorder::default)),
479            problems,
480        }
481    }
482
483    /// The recorder, while the load still records: the first time it is
484    /// asked for after a diagnostic, it is dropped, with all it holds.
485    fn recorder(&self) -> Option<RefMut<'_, Recorder>> {
486        let mut slot = self.recorder.borrow_mut();
487        if self.problems.get() > 0 {
488            *slot = None;
489        }
490        RefMut::filter_map(slot, Option::as_mut).ok()
491    }
492
493    /// The references recorded, if this load recorded them to the end.
494    pub fn into_recorder(self) -> Option<Recorder> {
495        let recorder = self.recorder.into_inner();
496        recorder.filter(|_| self.problems.get() == 0)
497    }
498
499    /// Records the profile's render layers.
500    pub fn render_layers_declared<'n>(&mut self, layers: impl IntoIterator<Item = &'n String>) {
501        self.render_layers.extend(layers.into_iter().cloned());
502    }
503
504    /// Records the profile's ground layers and definition types.
505    pub fn profile_declared<'n>(
506        &mut self,
507        ground_layers: impl IntoIterator<Item = &'n String>,
508        definition_types: impl IntoIterator<Item = &'n String>,
509    ) {
510        self.ground_layers
511            .extend(ground_layers.into_iter().cloned());
512        self.definition_types
513            .extend(definition_types.into_iter().cloned());
514    }
515
516    /// Claims an object uid for an object of the chunk at `path`; see
517    /// [`Claims::claim`].
518    pub fn claim_uid(&mut self, uid: &str, path: &Arc<str>) -> Option<Arc<str>> {
519        self.uids.claim(uid, path)
520    }
521
522    /// Records the definitions the `defs/` layout holds, before any is read.
523    pub fn definitions_listed(&mut self, keys: impl IntoIterator<Item = (String, String)>) {
524        for (kind, id) in keys {
525            self.definitions.entry(kind).or_default().insert(id);
526        }
527    }
528
529    /// Records the zones of `plane` that read, before their parents and
530    /// the plane's default zone are checked.
531    pub fn zones_read<'z>(&mut self, plane: &str, zones: impl IntoIterator<Item = &'z String>) {
532        self.zones
533            .entry(plane.to_owned())
534            .or_default()
535            .extend(zones.into_iter().cloned());
536    }
537
538    /// Records the terrains of the transitions document, before any
539    /// transition is read.
540    pub fn terrains_declared<'t>(&mut self, terrains: impl IntoIterator<Item = &'t String>) {
541        self.terrains.extend(terrains.into_iter().cloned());
542    }
543
544    /// Records a plane that loaded, with its anchors, before any chunk is
545    /// read.
546    pub fn plane_read(&mut self, plane: &Plane) {
547        self.planes
548            .insert(plane.id.clone(), plane.anchors.keys().cloned().collect());
549    }
550
551    /// Whether `target` resolves. A file always does here: whether it is
552    /// there is the reading's to report.
553    pub fn resolves(&self, target: &Target<'_>) -> bool {
554        match *target {
555            Target::RenderLayer(layer) => self.render_layers.contains(layer),
556            Target::GroundLayer(layer) => self.ground_layers.contains(layer),
557            Target::DefinitionType(kind) => self.definition_types.contains(kind),
558            Target::Definition { kind, id } => self
559                .definitions
560                .get(kind)
561                .is_some_and(|ids| ids.contains(id)),
562            Target::Plane(plane) => self.planes.contains_key(plane),
563            Target::Anchor { plane, id } => self
564                .planes
565                .get(plane)
566                .is_some_and(|anchors| anchors.contains(id)),
567            Target::Zone { plane, id } => self
568                .zones
569                .get(plane)
570                .is_some_and(|zones| zones.contains(id)),
571            Target::Terrain(terrain) => self.terrains.contains(terrain),
572            Target::File(_) => true,
573        }
574    }
575
576    /// The one check of a reference: records it when this load records
577    /// references, and says whether it resolves. The caller reports one
578    /// that does not, with [`message`](crate::index::message).
579    pub fn check(&self, site: &Site<'_>) -> bool {
580        self.follow(site);
581        self.resolves(&site.target)
582    }
583
584    /// Records a reference that the layout, not a lookup, checks: a path
585    /// read as a document, a plane directory, a definition's directory.
586    pub fn follow(&self, site: &Site<'_>) {
587        if let Some(mut recorder) = self.recorder() {
588            recorder.record(site);
589        }
590    }
591
592    /// The one check of a value of a schema `reference` field, as
593    /// [`References::check`]; `schema` holds its type, `field_type`, and
594    /// `path` is where the value is, written out only the first time a
595    /// value of that type is recorded.
596    fn check_field(
597        &self,
598        site: &Site<'_>,
599        types: (&Arc<Schema>, &FieldType),
600        path: &FieldPath<'_>,
601    ) -> bool {
602        if let Some(mut recorder) = self.recorder() {
603            recorder.record_field(site, types, path);
604        }
605        self.resolves(&site.target)
606    }
607
608    /// Checks a reference whose problem is reported on its file as a
609    /// whole, without a position; returns whether it resolves.
610    pub fn check_file(&self, site: &Site<'_>, diagnostics: &mut Diagnostics) -> bool {
611        let resolves = self.check(site);
612        if !resolves {
613            diagnostics.push(Diagnostic::file(site.file, Code::Reference, message(site)));
614        }
615        resolves
616    }
617}
618
619/// Where a value is in a `fields` object, for the field path a recorded
620/// reference carries: built on the stack as values are checked, and
621/// written out only when a reference is recorded.
622#[derive(Clone, Copy)]
623pub(crate) enum FieldPath<'p> {
624    Root,
625    Field(&'p FieldPath<'p>, &'p str),
626    Items(&'p FieldPath<'p>),
627}
628
629impl FieldPath<'_> {
630    /// The path as [`Role::Field`](crate::index::Role::Field) documents
631    /// it: names joined by `.`, with `[]` for a list's items.
632    pub(crate) fn render(&self) -> String {
633        match self {
634            Self::Root => String::new(),
635            Self::Field(parent, name) => match parent.render() {
636                parent if parent.is_empty() => (*name).to_owned(),
637                parent => format!("{parent}.{name}"),
638            },
639            Self::Items(parent) => format!("{}[]", parent.render()),
640        }
641    }
642}
643
644/// Validates a `fields` object against `schema`. Missing required fields are
645/// reported at `owner`, the object holding `fields`, and `holder` holds
646/// the references its values make.
647pub(crate) fn read_fields<'a>(
648    reader: &Reader<'a>,
649    (owner, holder): (&'a Value, Holder<'_>),
650    fields: Option<&'a Value>,
651    schema: &Arc<Schema>,
652    references: &mut References,
653) -> Option<Vec<(String, FieldValue)>> {
654    let members = match fields {
655        Some(fields) => reader.members(fields)?,
656        None => &[],
657    };
658    let mut output = Vec::with_capacity(members.len());
659    let at = Values {
660        holder,
661        schema,
662        path: &FieldPath::Root,
663    };
664    let ok = check_members(
665        reader,
666        members,
667        fields.unwrap_or(owner).span(),
668        schema,
669        (references, at),
670        &mut |member, spec| {
671            output.push((
672                member.key.clone(),
673                typed_value(reader, &member.value, &spec.field_type),
674            ));
675        },
676    );
677    ok.then_some(output)
678}
679
680/// Checks an object's members against `schema`: each key is one of its
681/// fields and each value has that field's type. Missing required fields are
682/// one diagnostic at `owner`. `accept` gets every member whose value is
683/// valid.
684///
685/// The work is proportional to the members the object has, however many
686/// fields the schema requires. An object with every required field is not
687/// checked for missing ones; naming the first few that another object lacks
688/// visits at most [`NAMED`] more required fields than it has.
689fn check_members<'a>(
690    reader: &Reader<'a>,
691    members: &'a [Member],
692    owner: Span,
693    schema: &Schema,
694    (references, at): (&mut References, Values<'_>),
695    accept: &mut dyn FnMut(&'a Member, &FieldSpec),
696) -> bool {
697    let mut ok = true;
698    // Positions of the required fields present. Keys are unique, so the
699    // owner lacks exactly the other required fields.
700    let mut present = BTreeSet::new();
701    for member in members {
702        let Some((position, spec)) = schema.lookup(&member.key) else {
703            reader.report(
704                member.key_span,
705                Code::UnknownKey,
706                format!("field \"{}\" is not in {}", member.key, schema.path),
707            );
708            ok = false;
709            continue;
710        };
711        if spec.required {
712            present.insert(position);
713        }
714        let path = FieldPath::Field(at.path, &member.key);
715        let at = Values { path: &path, ..at };
716        if check_value(reader, &member.value, &spec.field_type, (references, at)) {
717            accept(member, spec);
718        } else {
719            ok = false;
720        }
721    }
722    // One diagnostic per owner, however many fields it lacks.
723    let missing = schema.required.len() - present.len();
724    if missing > 0 {
725        let named: Vec<&str> = schema
726            .required
727            .iter()
728            .inspect(|_| visited())
729            .filter(|position| !present.contains(position))
730            .take(NAMED)
731            .filter_map(|position| schema.fields.get(*position))
732            .map(|(name, _)| name.as_str())
733            .collect();
734        reader.report(owner, Code::MissingKey, missing_message(&named, missing));
735        ok = false;
736    }
737    ok
738}
739
740/// Missing required fields named in one diagnostic; the rest are counted.
741const NAMED: usize = 3;
742
743#[cfg(test)]
744thread_local! {
745    /// Required fields visited on this thread while naming missing ones, so
746    /// tests can bound the work of a load.
747    pub(crate) static REQUIRED_VISITS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
748}
749
750/// Counts a required field visited while naming missing ones.
751#[cfg(test)]
752fn visited() {
753    REQUIRED_VISITS.with(|visits| visits.set(visits.get() + 1));
754}
755
756#[cfg(not(test))]
757const fn visited() {}
758
759/// The diagnostic for an owner that lacks `count` required fields, the
760/// first of which are `named`.
761fn missing_message(named: &[&str], count: usize) -> String {
762    let list = named
763        .iter()
764        .map(|name| format!("\"{name}\""))
765        .collect::<Vec<_>>()
766        .join(", ");
767    match count {
768        1 => format!("required field {list} is missing"),
769        count if count <= named.len() => format!("required fields {list} are missing"),
770        count => format!(
771            "required fields {list} and {} more are missing",
772            count - named.len()
773        ),
774    }
775}
776
777/// The model value of a field whose value has passed [`check_value`].
778fn typed_value(reader: &Reader<'_>, value: &Value, field_type: &FieldType) -> FieldValue {
779    match (field_type, value.as_f64()) {
780        (FieldType::Number { .. }, Some(number)) => FieldValue::Number(number),
781        _ => reader.field_value(value),
782    }
783}
784
785/// Where the values being checked are: what holds their references, and
786/// the field path of the value.
787#[derive(Clone, Copy)]
788struct Values<'p> {
789    holder: Holder<'p>,
790    /// The schema document's own schema, which holds every type checked.
791    schema: &'p Arc<Schema>,
792    path: &'p FieldPath<'p>,
793}
794
795/// Checks that `value` has type `field_type`, reporting every problem in
796/// it, nested ones included. Recursion follows the type, whose nesting
797/// the schema bounds by [`MAXIMUM_FIELD_DEPTH`].
798fn check_value<'a>(
799    reader: &Reader<'a>,
800    value: &'a Value,
801    field_type: &FieldType,
802    (references, at): (&mut References, Values<'_>),
803) -> bool {
804    match field_type {
805        FieldType::Boolean => reader.boolean(value).is_some(),
806        FieldType::Integer { minimum, maximum } => {
807            reader.integer(value, *minimum, *maximum).is_some()
808        }
809        FieldType::Number { minimum, maximum } => {
810            let Some(number) = value.as_f64() else {
811                reader.report(
812                    value.span(),
813                    Code::Type,
814                    format!("expected a number, found {}", value.kind()),
815                );
816                return false;
817            };
818            if number < *minimum || number > *maximum {
819                reader.report(
820                    value.span(),
821                    Code::Range,
822                    format!("{number} is outside {minimum}..={maximum}"),
823                );
824                return false;
825            }
826            true
827        }
828        FieldType::String {
829            maximum_length,
830            values,
831        } => {
832            let Some(text) = reader.text(value, *maximum_length) else {
833                return false;
834            };
835            if !values.is_empty() && !values.contains(&text) {
836                reader.report(
837                    value.span(),
838                    Code::Range,
839                    format!("\"{text}\" is not one of the allowed values"),
840                );
841                return false;
842            }
843            true
844        }
845        FieldType::Reference { target } => {
846            let Some(id) = reader.string(value) else {
847                return false;
848            };
849            if !is_identifier(id) {
850                reader.report(
851                    value.span(),
852                    Code::Identifier,
853                    format!("\"{id}\" is not a valid identifier"),
854                );
855                return false;
856            }
857            // The field path is the recorder's to write out, once a type.
858            let site = reader.site(
859                Target::Definition { kind: target, id },
860                (Role::Field { field: "" }, at.holder),
861                value.span(),
862            );
863            if !references.check_field(&site, (at.schema, field_type), at.path) {
864                reader.report(value.span(), Code::Reference, message(&site));
865            }
866            true
867        }
868        FieldType::Json => true,
869        FieldType::List {
870            items,
871            minimum_items,
872            maximum_items,
873        } => check_list(
874            reader,
875            value,
876            items,
877            *minimum_items..=*maximum_items,
878            (references, at),
879        ),
880        FieldType::Object { fields } => {
881            let Some(members) = reader.members(value) else {
882                return false;
883            };
884            check_members(
885                reader,
886                members,
887                value.span(),
888                fields,
889                (references, at),
890                &mut |_, _| {},
891            )
892        }
893    }
894}
895
896/// Checks a `list` value: an array of `count` items, each of type `items`.
897/// Every item is checked, even when the count is wrong, so each bad one is
898/// reported.
899fn check_list<'a>(
900    reader: &Reader<'a>,
901    value: &'a Value,
902    items: &FieldType,
903    count: std::ops::RangeInclusive<usize>,
904    (references, at): (&mut References, Values<'_>),
905) -> bool {
906    let Some(elements) = reader.array(value) else {
907        return false;
908    };
909    let mut ok = count.contains(&elements.len());
910    if !ok {
911        reader.report(
912            value.span(),
913            Code::Range,
914            format!(
915                "{} items, outside {}..={}",
916                elements.len(),
917                count.start(),
918                count.end()
919            ),
920        );
921    }
922    let path = FieldPath::Items(at.path);
923    let at = Values { path: &path, ..at };
924    for element in elements {
925        ok &= check_value(reader, element, items, (references, at));
926    }
927    ok
928}
929
930#[cfg(test)]
931mod tests {
932    use super::{References, Target};
933    use std::path::Path;
934
935    #[test]
936    fn references_resolve_against_what_is_known_before_they_are_read() {
937        let world =
938            crate::load(&Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/basic"))
939                .expect("the fixture is valid");
940        let mut references = References::new(false, std::rc::Rc::default());
941        references.render_layers_declared(&world.profile().render_layers);
942        let proto = |id| Target::Definition { kind: "proto", id };
943        let anchor = Target::Anchor {
944            plane: "overworld",
945            id: "village.well",
946        };
947        assert!(references.resolves(&Target::RenderLayer("wall")));
948        assert!(!references.resolves(&Target::RenderLayer("roof")));
949        assert!(!references.resolves(&proto("tree")));
950        assert!(!references.resolves(&anchor));
951        references.definitions_listed(world.definitions().keys().cloned());
952        references.plane_read(&world.planes()["overworld"]);
953        assert!(references.resolves(&proto("tree")));
954        assert!(!references.resolves(&proto("missing")));
955        assert!(references.resolves(&anchor));
956        assert!(references.resolves(&Target::Plane("overworld")));
957        assert!(!references.resolves(&Target::Plane("vault")));
958        assert!(!references.resolves(&Target::Zone {
959            plane: "overworld",
960            id: "village"
961        }));
962        references.zones_read("overworld", world.planes()["overworld"].zones.keys());
963        assert!(references.resolves(&Target::Zone {
964            plane: "overworld",
965            id: "village"
966        }));
967        assert!(!references.resolves(&Target::Zone {
968            plane: "vault",
969            id: "village"
970        }));
971    }
972}