1use 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
22pub const DEFAULT_MAXIMUM_STRING: usize = 65_536;
24
25pub const MAXIMUM_FIELD_DEPTH: usize = 8;
29
30pub const MAXIMUM_LIST_ITEMS: usize = 250_000;
33
34#[derive(Clone, Debug, PartialEq)]
37pub struct Schema {
38 pub path: Arc<str>,
41 fields: Vec<(String, FieldSpec)>,
42 index: BTreeMap<String, usize>,
44 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 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 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 #[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 String {
107 maximum_length: usize,
108 values: BTreeSet<String>,
109 },
110 Reference {
112 target: String,
113 },
114 Json,
116 List {
119 items: Box<FieldType>,
120 minimum_items: usize,
121 maximum_items: usize,
122 },
123 Object {
125 fields: Schema,
126 },
127}
128
129impl FieldType {
130 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
147fn 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
158pub(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
182fn 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 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#[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 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 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 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
398fn 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
414fn 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
431pub(crate) struct References {
445 render_layers: BTreeSet<String>,
446 ground_layers: BTreeSet<String>,
447 definition_types: BTreeSet<String>,
448 definitions: BTreeMap<String, BTreeSet<String>>,
450 planes: BTreeMap<String, BTreeSet<String>>,
452 zones: BTreeMap<String, BTreeSet<String>>,
454 terrains: BTreeSet<String>,
456 uids: Claims,
457 recorder: RefCell<Option<Recorder>>,
460 problems: Rc<Cell<usize>>,
463}
464
465impl References {
466 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 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 pub fn into_recorder(self) -> Option<Recorder> {
495 let recorder = self.recorder.into_inner();
496 recorder.filter(|_| self.problems.get() == 0)
497 }
498
499 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 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 pub fn claim_uid(&mut self, uid: &str, path: &Arc<str>) -> Option<Arc<str>> {
519 self.uids.claim(uid, path)
520 }
521
522 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 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 pub fn terrains_declared<'t>(&mut self, terrains: impl IntoIterator<Item = &'t String>) {
541 self.terrains.extend(terrains.into_iter().cloned());
542 }
543
544 pub fn plane_read(&mut self, plane: &Plane) {
547 self.planes
548 .insert(plane.id.clone(), plane.anchors.keys().cloned().collect());
549 }
550
551 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 pub fn check(&self, site: &Site<'_>) -> bool {
580 self.follow(site);
581 self.resolves(&site.target)
582 }
583
584 pub fn follow(&self, site: &Site<'_>) {
587 if let Some(mut recorder) = self.recorder() {
588 recorder.record(site);
589 }
590 }
591
592 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 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#[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 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
644pub(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
680fn 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 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 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
740const NAMED: usize = 3;
742
743#[cfg(test)]
744thread_local! {
745 pub(crate) static REQUIRED_VISITS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
748}
749
750#[cfg(test)]
752fn visited() {
753 REQUIRED_VISITS.with(|visits| visits.set(visits.get() + 1));
754}
755
756#[cfg(not(test))]
757const fn visited() {}
758
759fn 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
777fn 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#[derive(Clone, Copy)]
788struct Values<'p> {
789 holder: Holder<'p>,
790 schema: &'p Arc<Schema>,
792 path: &'p FieldPath<'p>,
793}
794
795fn 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 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
896fn 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}