Skip to main content

yog_data/schema/
mod.rs

1//! Schemas: a format is a Rust type implementing [`Schema`] (usually
2//! `#[derive(Schema)]`). One schema reads and writes the format, checks it
3//! with the path of every problem, collects its references for the build to
4//! check, and describes itself ([`Shape`]) for Yog-IDLE's editors and the
5//! docs.
6
7mod fields;
8mod one_or_list;
9mod plain;
10mod reader;
11mod shape;
12mod typed_values;
13
14pub use fields::{Fields, field_name};
15pub use one_or_list::OneOrList;
16pub use plain::*;
17pub use reader::*;
18pub use shape::*;
19pub use typed_values::*;
20
21use std::collections::{BTreeMap, HashMap};
22use std::fmt;
23use std::marker::PhantomData;
24
25use crate::binary::{Decoder, Writer};
26use crate::borrowed::{MapRef, ValueRef};
27use crate::frame::{Packer, Unpacker};
28use crate::text::{self, Doc, LineCol};
29use crate::typed;
30use crate::value::{Map, Path, Ref, Res, Seg, Value};
31
32/// A Yog Data format.
33pub trait Schema: Sized {
34    /// What the format looks like.
35    fn shape() -> Shape;
36
37    /// Reads `v`. Problems go to `cx` at its current path; `None` when the
38    /// value couldn't be read (the problem is in `cx`).
39    fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self>;
40
41    /// The value, as the format writes it.
42    fn write(&self) -> Value;
43
44    /// The value of a field that isn't there — `None`: the field is required.
45    fn absent() -> Option<Self> {
46        None
47    }
48
49    /// Whether a struct leaves this field out when writing it.
50    fn skip(&self) -> bool {
51        false
52    }
53
54    /// Writes itself in the binary form, straight — [`Schema::write`]'s
55    /// value encoded, unless the type knows better (derived ones do).
56    fn encode(&self, w: &mut Writer) {
57        w.value(&self.write());
58    }
59
60    /// Reads itself from the binary form, straight; `None` when it isn't
61    /// what [`Schema::encode`] writes — then [`read_bytes`] reads the slow
62    /// way, which says what's wrong. Through a value unless the type knows
63    /// better (derived ones do).
64    fn decode(d: &mut Decoder<'_>) -> Option<Self> {
65        let arena = bumpalo::Bump::new();
66        let v = d.value_ref(&arena).ok()?;
67        read_ref_value(&v).ok()
68    }
69
70    /// Packs itself into a frame ([`crate::frame`]): its binary form as
71    /// bytes, unless the type packs itself (derived ones and plain types do).
72    fn pack(&self, p: &mut Packer) {
73        p.put_tagged(self);
74    }
75
76    /// Reads itself out of a frame; `None` if the bytes aren't what
77    /// [`Schema::pack`] writes.
78    fn unpack(u: &mut Unpacker<'_>) -> Option<Self> {
79        u.get_tagged()
80    }
81
82    /// A list of these packed: a count, then each one (floats raw).
83    fn pack_list(items: &[Self], p: &mut Packer) {
84        let at = p.begin_list(items.len());
85        for item in items {
86            item.pack(p);
87        }
88        p.end(at);
89    }
90
91    /// A list of these out of a frame.
92    fn unpack_list(u: &mut Unpacker<'_>) -> Option<Vec<Self>> {
93        let (n, end) = u.list()?;
94        let mut out = Vec::with_capacity(n);
95        for _ in 0..n {
96            out.push(Self::unpack(u)?);
97        }
98        u.skip_to(end)?;
99        Some(out)
100    }
101
102    /// A list of these written straight (numbers pack theirs).
103    fn encode_list(items: &[Self], w: &mut Writer) {
104        w.list(items.len());
105        for item in items {
106            item.encode(w);
107        }
108    }
109
110    /// A list of these read straight.
111    fn decode_list(d: &mut Decoder<'_>) -> Option<Vec<Self>> {
112        let n = d.list()?;
113        let mut out = Vec::with_capacity(n);
114        for _ in 0..n {
115            out.push(Self::decode(d)?);
116        }
117        d.leave();
118        Some(out)
119    }
120}
121
122/// Where a tagged enum's variant keeps what it wraps when that isn't a
123/// struct (`Id(String)` is `{type: "id", value: "…"}`); a wrapped struct's
124/// fields sit next to the tag instead.
125pub const WRAPPED: &str = yog_ids::word::VALUE;
126
127/// Whether a tagged enum's variant wrapping `T` lays `T`'s fields next to
128/// the tag (a struct) or keeps `T` under [`WRAPPED`].
129#[doc(hidden)]
130pub fn flattens<T: Schema>() -> bool {
131    matches!(T::shape(), Shape::Struct(_))
132}
133
134/// `v` in the binary form, written straight ([`Schema::encode`]).
135pub fn to_bytes<T: Schema>(v: &T) -> Vec<u8> {
136    let mut w = Writer::new();
137    v.encode(&mut w);
138    w.finish()
139}
140
141/// The field names of a struct's shape (none for anything else).
142pub fn field_names(shape: &Shape) -> Vec<&'static str> {
143    match shape {
144        Shape::Struct(s) => s.fields.iter().map(|f| f.name).collect(),
145        _ => Vec::new(),
146    }
147}
148
149/// `A` and `B` read from the same object — each allowing the other's
150/// fields, like serde's `flatten`: a recipe's common fields and its kind's.
151pub fn read_both<A: Schema, B: Schema>(
152    v: &ValueRef,
153    cx: &mut Reader,
154    also: &[&'static str],
155) -> Option<(A, B)> {
156    let (an, bn) = (field_names(&A::shape()), field_names(&B::shape()));
157    let mut b_keys = bn.clone();
158    b_keys.extend_from_slice(also);
159    let mut a_keys = an;
160    a_keys.extend_from_slice(also);
161    let a = cx.with_allowed_all(&b_keys, |cx| A::read(v, cx));
162    let b = cx.with_allowed_all(&a_keys, |cx| B::read(v, cx));
163    Some((a?, b?))
164}
165
166/// Reads `v` as `T`: the value and its references, or every problem.
167pub fn from_value<T: Schema>(v: &Value) -> Result<Read<T>, Vec<Problem>> {
168    let arena = bumpalo::Bump::new();
169    read_with(&v.borrow(&arena), Reader::new())
170}
171
172/// Reads `v` as `T`, nothing else: what the game wants of content (the
173/// references met aren't kept).
174pub fn read_value<T: Schema>(v: &Value) -> Result<T, Vec<Problem>> {
175    let arena = bumpalo::Bump::new();
176    read_ref_value(&v.borrow(&arena))
177}
178
179/// [`read_value`] of a borrowed value.
180pub fn read_ref_value<T: Schema>(v: &ValueRef) -> Result<T, Vec<Problem>> {
181    let reader = Reader {
182        value_only: true,
183        ..Reader::new()
184    };
185    read_with(v, reader).map(|r| r.value)
186}
187
188/// Reads the binary form as `T`, borrowed all the way: no string is
189/// copied but into `T` itself — content as the runtime takes it.
190pub fn read_bytes<T: Schema>(bytes: &[u8]) -> Result<T, Vec<Problem>> {
191    // Straight, the usual case; the slow way says what's wrong.
192    if let Ok(mut d) = Decoder::open(bytes)
193        && let Some(v) = T::decode(&mut d)
194        && d.at_end()
195    {
196        return Ok(v);
197    }
198    let arena = bumpalo::Bump::new();
199    let v = crate::binary::decode_ref(bytes, &arena).map_err(|e| {
200        vec![Problem {
201            path: Path::root(),
202            code: yog_ids::problem_code::BAD_BINARY,
203            message: e.to_string(),
204            at_key: false,
205        }]
206    })?;
207    read_ref_value(&v)
208}
209
210/// `from_value`, references and texts going through `resolver`.
211pub fn from_value_with<T: Schema>(
212    v: &Value,
213    resolver: &mut dyn Resolve,
214) -> Result<Read<T>, Vec<Problem>> {
215    let arena = bumpalo::Bump::new();
216    read_with(&v.borrow(&arena), Reader::with_resolver(resolver))
217}
218
219fn read_with<T: Schema>(v: &ValueRef, mut cx: Reader<'_>) -> Result<Read<T>, Vec<Problem>> {
220    let value = T::read(v, &mut cx);
221    match value {
222        Some(value) if cx.problems.is_empty() => Ok(Read {
223            value,
224            refs: cx.refs,
225            tags: cx.tags,
226            resources: cx.resources,
227        }),
228        _ => Err(cx.problems),
229    }
230}
231
232/// A table a crate ships (`include_str!` of its `.ydata`), read: what it
233/// says, or — when it won't read, which its crate's tests catch — the
234/// error logged (`what`: which table) and nothing.
235pub fn built_in<T: Schema + Default>(what: &str, text: &str) -> T {
236    match from_text::<T>(text) {
237        Ok(read) => read.value,
238        Err(e) => {
239            yog_logging::error!("{what} won't read: {e}");
240            T::default()
241        }
242    }
243}
244
245/// Parses `text` and reads it as `T`; problems come with their line and column.
246pub fn from_text<T: Schema>(text: &str) -> Result<Read<T>, Errors> {
247    let doc = text::parse(text).map_err(|e| {
248        Errors(vec![Located {
249            at: Some(e.at),
250            path: Path::root(),
251            message: e.message,
252        }])
253    })?;
254    from_doc(text, &doc)
255}
256
257/// Parses TOML `text` and reads it as `T`.
258#[cfg(feature = "toml")]
259pub fn from_toml<T: Schema>(text: &str) -> Result<Read<T>, Errors> {
260    let doc = crate::toml::parse(text).map_err(|e| {
261        Errors(vec![Located {
262            at: Some(e.at),
263            path: Path::root(),
264            message: e.message,
265        }])
266    })?;
267    from_doc(text, &doc)
268}
269
270/// Reads a parsed document as `T`; `text` is its source, for positions.
271pub fn from_doc<T: Schema>(text: &str, doc: &Doc) -> Result<Read<T>, Errors> {
272    locate(text, doc, from_value(&doc.value))
273}
274
275/// `from_doc`, references and texts going through `resolver`.
276pub fn from_doc_with<T: Schema>(
277    text: &str,
278    doc: &Doc,
279    resolver: &mut dyn Resolve,
280) -> Result<Read<T>, Errors> {
281    locate(text, doc, from_value_with(&doc.value, resolver))
282}
283
284fn locate<T>(
285    text: &str,
286    doc: &Doc,
287    read: Result<Read<T>, Vec<Problem>>,
288) -> Result<Read<T>, Errors> {
289    read.map_err(|problems| {
290        let mut located: Vec<(u32, Located)> = problems
291            .into_iter()
292            .map(|p| {
293                let spans = doc.spans_at(&p.path);
294                let span = if p.at_key {
295                    spans.key.unwrap_or(spans.span)
296                } else {
297                    spans.span
298                };
299                (
300                    span.start,
301                    Located {
302                        at: Some(LineCol::of(text, span.start as usize)),
303                        path: p.path,
304                        message: p.message,
305                    },
306                )
307            })
308            .collect();
309        // In file order, as an editor lists them.
310        located.sort_by_key(|(offset, _)| *offset);
311        Errors(located.into_iter().map(|(_, l)| l).collect())
312    })
313}
314
315/// `value` as text, indented.
316pub fn to_text<T: Schema>(value: &T) -> String {
317    text::to_string_pretty(&value.write())
318}