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