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