Skip to main content

tilewright_source/
read.rs

1// Copyright 2026 Seraphina Studios
2// SPDX-License-Identifier: MIT
3
4//! Shape checks shared by every document reader. Each helper reports its
5//! own diagnostic and returns `None` when the value is unusable.
6
7use crate::diagnostic::Diagnostics;
8use crate::{
9    Code, Diagnostic, FieldValue,
10    diagnostic::Positions,
11    files::Source,
12    index::{Holder, Role, Site, Target, message},
13    schema::References,
14};
15use std::{
16    cell::{OnceCell, RefCell},
17    collections::BTreeSet,
18};
19use tilewright_jsonc::{MAX_SAFE_INTEGER, Member, Span, Value};
20
21/// Whether `text` is an identifier: it matches `^[a-z0-9][a-z0-9_.-]{0,127}$`.
22#[must_use]
23pub fn is_identifier(text: &str) -> bool {
24    let bytes = text.as_bytes();
25    !bytes.is_empty()
26        && bytes.len() <= 128
27        && (bytes[0].is_ascii_lowercase() || bytes[0].is_ascii_digit())
28        && bytes
29            .iter()
30            .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || b"_.-".contains(byte))
31}
32
33/// Whether `text` is a domain-qualified namespace for `custom` data, such
34/// as `example.org` or `example.org/tool`.
35#[must_use]
36pub fn is_namespace(text: &str) -> bool {
37    let (domain, rest) = text.split_once('/').unwrap_or((text, ""));
38    domain.contains('.')
39        && is_identifier(domain)
40        && !domain.starts_with('.')
41        && !domain.ends_with('.')
42        && (rest.is_empty() || is_identifier(rest))
43}
44
45/// Reads one document. Diagnostics go to a shared list through a
46/// `RefCell`, so helpers can nest (`reader.i32(reader.required(..)?)`).
47pub(crate) struct Reader<'a> {
48    pub source: &'a Source,
49    diagnostics: RefCell<&'a mut Diagnostics>,
50    /// Built at the first diagnostic.
51    positions: OnceCell<Positions<'a>>,
52}
53
54impl<'a> Reader<'a> {
55    pub fn new(source: &'a Source, diagnostics: &'a mut Diagnostics) -> Self {
56        Self {
57            source,
58            diagnostics: RefCell::new(diagnostics),
59            positions: OnceCell::new(),
60        }
61    }
62
63    fn positions(&self) -> &Positions<'a> {
64        self.positions
65            .get_or_init(|| Positions::new(self.source.document.source()))
66    }
67
68    /// Checks a reference of this document at `span`, reporting it there
69    /// when it does not resolve; returns whether it resolves.
70    pub fn reference(
71        &self,
72        references: &References,
73        target: Target<'_>,
74        site: (Role<'_>, Holder<'_>),
75        span: Span,
76    ) -> bool {
77        self.reference_reported_at(references, target, site, span, span)
78    }
79
80    /// Checks a reference of this document at `span` as
81    /// [`Reader::reference`] does, reporting it at `report` instead, as
82    /// for a palette entry, which is reported at its grid.
83    pub fn reference_reported_at(
84        &self,
85        references: &References,
86        target: Target<'_>,
87        (role, holder): (Role<'_>, Holder<'_>),
88        span: Span,
89        report: Span,
90    ) -> bool {
91        let site = self.site(target, (role, holder), span);
92        let resolves = references.check(&site);
93        if !resolves {
94            self.report(report, Code::Reference, message(&site));
95        }
96        resolves
97    }
98
99    /// A reference of this document at `span`.
100    pub fn site<'s>(
101        &'s self,
102        target: Target<'s>,
103        (role, holder): (Role<'s>, Holder<'s>),
104        span: Span,
105    ) -> Site<'s> {
106        Site {
107            target,
108            role,
109            holder,
110            file: &self.source.path,
111            start: span.start,
112            end: span.end,
113        }
114    }
115
116    pub fn root(&self) -> &'a Value {
117        self.source.document.root()
118    }
119
120    pub fn canonical(&self, value: &Value) -> String {
121        self.source.document.canonical(value)
122    }
123
124    /// Builds a diagnostic at `span` without recording it.
125    pub fn diagnostic(&self, span: Span, code: Code, message: impl Into<String>) -> Diagnostic {
126        Diagnostic::at(
127            &self.source.path,
128            self.positions(),
129            span.start,
130            span.end,
131            code,
132            message,
133        )
134    }
135
136    pub fn report(&self, span: Span, code: Code, message: impl Into<String>) {
137        let diagnostic = self.diagnostic(span, code, message);
138        self.diagnostics.borrow_mut().push(diagnostic);
139    }
140
141    /// An object whose keys are all in `allowed`; unknown keys are reported.
142    pub fn object(&self, value: &'a Value, allowed: &[&str]) -> Option<&'a Value> {
143        let Some(members) = value.as_object() else {
144            self.report(
145                value.span(),
146                Code::Type,
147                format!("expected an object, found {}", value.kind()),
148            );
149            return None;
150        };
151        for member in members {
152            if !allowed.contains(&member.key.as_str()) {
153                self.report(
154                    member.key_span,
155                    Code::UnknownKey,
156                    format!("unknown key \"{}\"", member.key),
157                );
158            }
159        }
160        Some(value)
161    }
162
163    /// The members of an open map: an object with any keys.
164    pub fn members(&self, value: &'a Value) -> Option<&'a [Member]> {
165        let members = value.as_object();
166        if members.is_none() {
167            self.report(
168                value.span(),
169                Code::Type,
170                format!("expected an object, found {}", value.kind()),
171            );
172        }
173        members
174    }
175
176    pub fn required(&self, object: &'a Value, key: &str) -> Option<&'a Value> {
177        let value = object.get(key);
178        if value.is_none() {
179            self.report(
180                object.span(),
181                Code::MissingKey,
182                format!("missing key \"{key}\""),
183            );
184        }
185        value
186    }
187
188    /// Checks the document's `format` key.
189    pub fn format(&self, root: &'a Value, expected: &str) -> bool {
190        match self.required(root, "format") {
191            Some(value) if value.as_str() == Some(expected) => true,
192            Some(value) => {
193                self.report(
194                    value.span(),
195                    Code::Format,
196                    format!("format must be \"{expected}\""),
197                );
198                false
199            }
200            None => false,
201        }
202    }
203
204    pub fn string(&self, value: &'a Value) -> Option<&'a str> {
205        let text = value.as_str();
206        if text.is_none() {
207            self.report(
208                value.span(),
209                Code::Type,
210                format!("expected a string, found {}", value.kind()),
211            );
212        }
213        text
214    }
215
216    /// A string of at most `maximum` characters.
217    pub fn text(&self, value: &'a Value, maximum: usize) -> Option<String> {
218        let text = self.string(value)?;
219        if text.chars().count() > maximum {
220            self.report(
221                value.span(),
222                Code::Range,
223                format!("longer than {maximum} characters"),
224            );
225            return None;
226        }
227        Some(text.to_owned())
228    }
229
230    pub fn identifier(&self, value: &'a Value) -> Option<String> {
231        let text = self.string(value)?;
232        if is_identifier(text) {
233            Some(text.to_owned())
234        } else {
235            self.report(
236                value.span(),
237                Code::Identifier,
238                format!("\"{text}\" is not a valid identifier"),
239            );
240            None
241        }
242    }
243
244    pub fn boolean(&self, value: &'a Value) -> Option<bool> {
245        let flag = value.as_bool();
246        if flag.is_none() {
247            self.report(
248                value.span(),
249                Code::Type,
250                format!("expected a boolean, found {}", value.kind()),
251            );
252        }
253        flag
254    }
255
256    /// An integer in `minimum..=maximum`.
257    pub fn integer(&self, value: &'a Value, minimum: i64, maximum: i64) -> Option<i64> {
258        let Some(number) = value.as_i64() else {
259            self.report(
260                value.span(),
261                Code::Type,
262                format!("expected an integer, found {}", value.kind()),
263            );
264            return None;
265        };
266        if (minimum..=maximum).contains(&number) {
267            Some(number)
268        } else {
269            self.report(
270                value.span(),
271                Code::Range,
272                format!("{number} is outside {minimum}..={maximum}"),
273            );
274            None
275        }
276    }
277
278    pub fn i32(&self, value: &'a Value) -> Option<i32> {
279        self.integer(value, i64::from(i32::MIN), i64::from(i32::MAX))
280            .and_then(|number| i32::try_from(number).ok())
281    }
282
283    pub fn u32(&self, value: &'a Value, minimum: u32) -> Option<u32> {
284        self.integer(value, i64::from(minimum), i64::from(u32::MAX))
285            .and_then(|number| u32::try_from(number).ok())
286    }
287
288    pub fn array(&self, value: &'a Value) -> Option<&'a [Value]> {
289        let items = value.as_array();
290        if items.is_none() {
291            self.report(
292                value.span(),
293                Code::Type,
294                format!("expected an array, found {}", value.kind()),
295            );
296        }
297        items
298    }
299
300    /// A pair of integers within the format's integer range.
301    pub fn coordinates(&self, value: &'a Value) -> Option<[i64; 2]> {
302        if let [x, y] = self.array(value)? {
303            // Both are checked before either is used.
304            let x = self.integer(x, -MAX_SAFE_INTEGER, MAX_SAFE_INTEGER);
305            let y = self.integer(y, -MAX_SAFE_INTEGER, MAX_SAFE_INTEGER);
306            Some([x?, y?])
307        } else {
308            self.report(value.span(), Code::Type, "expected [x, y]");
309            None
310        }
311    }
312
313    /// A list of unique identifiers. Callers cap the list's length first.
314    pub fn identifiers(&self, value: &'a Value) -> Option<Vec<String>> {
315        let items = self.array(value)?;
316        let mut seen = BTreeSet::new();
317        let mut output = Vec::with_capacity(items.len());
318        for item in items {
319            let identifier = self.identifier(item)?;
320            if !seen.insert(identifier.clone()) {
321                self.report(
322                    item.span(),
323                    Code::Duplicate,
324                    format!("\"{identifier}\" is listed twice"),
325                );
326                return None;
327            }
328            output.push(identifier);
329        }
330        Some(output)
331    }
332
333    /// A `custom` object whose keys are domain-qualified namespaces.
334    pub fn custom(&self, object: &'a Value) -> Option<String> {
335        let value = object.get("custom")?;
336        let members = value.as_object();
337        if members.is_none() {
338            self.report(value.span(), Code::Type, "custom must be an object");
339            return None;
340        }
341        for member in members.unwrap_or_default() {
342            if !is_namespace(&member.key) {
343                self.report(
344                    member.key_span,
345                    Code::Identifier,
346                    format!(
347                        "custom key \"{}\" must be a domain-qualified namespace",
348                        member.key
349                    ),
350                );
351            }
352        }
353        Some(self.canonical(value))
354    }
355
356    /// The typed form of a value as a field value.
357    pub fn field_value(&self, value: &Value) -> FieldValue {
358        match value {
359            Value::Null { .. } => FieldValue::Null,
360            Value::Bool { value, .. } => FieldValue::Bool(*value),
361            Value::Integer { value, .. } => FieldValue::Integer(*value),
362            Value::Float { value, .. } => FieldValue::Number(*value),
363            Value::String { value, .. } => FieldValue::String(value.clone()),
364            Value::Array { .. } | Value::Object { .. } => FieldValue::Json(self.canonical(value)),
365        }
366    }
367}