1use 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
19pub const DEFAULT_MAXIMUM_STRING: usize = 65_536;
21
22pub const MAXIMUM_FIELD_DEPTH: usize = 8;
26
27pub const MAXIMUM_LIST_ITEMS: usize = 250_000;
30
31#[derive(Clone, Debug, PartialEq)]
34pub struct Schema {
35 pub path: Arc<str>,
38 fields: Vec<(String, FieldSpec)>,
39 index: BTreeMap<String, usize>,
41 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 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 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 #[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 String {
104 maximum_length: usize,
105 values: BTreeSet<String>,
106 },
107 Reference {
109 target: String,
110 },
111 Json,
113 List {
116 items: Box<FieldType>,
117 minimum_items: usize,
118 maximum_items: usize,
119 },
120 Object {
122 fields: Schema,
123 },
124}
125
126impl FieldType {
127 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
144fn 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
155pub(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
169fn 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 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#[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 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 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 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
380fn 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
396fn 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#[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 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
436pub(crate) struct References {
443 render_layers: BTreeSet<String>,
444 definitions: BTreeMap<String, BTreeSet<String>>,
446 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 pub fn claim_uid(&mut self, uid: &str, path: &Arc<str>) -> Option<Arc<str>> {
464 self.uids.claim(uid, path)
465 }
466
467 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 pub fn plane_read(&mut self, plane: &Plane) {
477 self.planes
478 .insert(plane.id.clone(), plane.anchors.keys().cloned().collect());
479 }
480
481 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
498pub(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
528fn 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 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 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
586const NAMED: usize = 3;
588
589#[cfg(test)]
590thread_local! {
591 pub(crate) static REQUIRED_VISITS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
594}
595
596#[cfg(test)]
598fn visited() {
599 REQUIRED_VISITS.with(|visits| visits.set(visits.get() + 1));
600}
601
602#[cfg(not(test))]
603const fn visited() {}
604
605fn 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
623fn 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
631fn 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
731fn 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}