Skip to main content

omnist/formats/
mod.rs

1//! Codecs over the canonical Document model. Ported from
2//! `~/dev/omnist/omnist/formats.py`; each format's reader parses text into
3//! [`crate::document::Doc`] and its writer projects a `Doc` back to text.
4//!
5//! Unlike [`crate::oml`] (omnist's own format, always lossless), JSON/YAML/
6//! TOML/XML can each fail to hold some value losslessly. Writing is
7//! **lenient by default**: the writer adjusts the value and records the
8//! change in a [`crate::report::WriteReport`]; `strict = true` raises
9//! [`crate::error::WriteError`] (carrying the report) instead. See
10//! [`crate::report`].
11//!
12//! This issue (#16) added the first of the four: [`json`]. Issue #18 added
13//! [`yaml`]; issue #20 added [`toml`]; issue #22 adds [`xml`], the last and
14//! structurally different one -- see `xml.rs`'s own doc comment.
15
16pub(crate) mod float_fmt;
17pub(crate) mod int_cap;
18pub mod json;
19pub(crate) mod string_escape;
20pub(crate) mod textpos;
21pub mod toml;
22pub mod xml;
23pub mod yaml;
24
25use crate::document::{Doc, Value};
26use crate::error::OmnistError;
27use crate::report::WriteReport;
28
29/// The (read, write, check) contract every builtin format codec already
30/// implements as a naming convention -- expressed once (issue #52).
31///
32/// `read`/`write`/`check` intentionally match the registry's
33/// [`crate::registry::ReadFn`]/[`crate::registry::WriteFn`]/
34/// [`crate::registry::CheckFn`] signatures exactly: no `strict`, no
35/// `report`, no format-specific options (`write_json`'s `indent`,
36/// `write_oml`'s `RawNode`/`indent` shape). Each format's richer public
37/// `read_X`/`write_X(doc, ..., strict, report)`/`check_X` functions are
38/// unchanged and remain the actual public API -- a `Codec` impl is thin
39/// `pub(crate)` plumbing that calls them with the registry's documented
40/// defaults (`indent: None`, `strict: false`, no report requested; OML's
41/// `Doc::from_raw`/`to_raw` bridging), exactly what the hand-written
42/// wrapper closures in `registry::builtins` did before this issue -- see
43/// `registry.rs`'s module doc for why those defaults are the right ones
44/// and why the registry's signatures are simpler than the public
45/// functions'.
46///
47/// A richer trait sketch (scan/emit split, `strict`/`report`-aware
48/// `write`) was considered and rejected: `write_json` needs `strict` to
49/// pick lenient vs. strict *content* (NaN/Infinity substitution), not just
50/// whether `finish_write` raises, so a `strict`-unaware `emit` provided
51/// method would be wrong for JSON specifically and each impl would have to
52/// override `write` anyway -- collapsing the supposed savings. This
53/// leaner shape captures the actual duplication (the registry's adapter
54/// closures) without forcing an artificial decomposition that fights each
55/// format's real differences.
56pub(crate) trait Codec: 'static {
57    /// The name this codec is registered under (`"json"`, `"yaml"`, ...).
58    const NAME: &'static str;
59
60    fn read(text: &str) -> Result<Doc, OmnistError>;
61    fn write(doc: &Doc) -> Result<String, OmnistError>;
62    fn check(doc: &Doc) -> WriteReport;
63
64    /// Build this codec's [`crate::registry::Format`] entry -- the
65    /// wrapper-closure boilerplate `registry::builtins` used to hand-write
66    /// once per format, now written once here instead.
67    fn format() -> crate::registry::Format {
68        crate::registry::Format::new(Self::NAME, Self::read, Self::write).with_check(Self::check)
69    }
70}
71
72/// One position `visit_grouped` reaches, passed to its callback alongside
73/// the current path. Kept as a single enum (rather than two separate
74/// closures) so callers only need one `&mut` capture of their accumulator
75/// (e.g. a `WriteReport`) -- two closures both borrowing the same
76/// `&mut WriteReport` for the whole walk don't borrow-check, since both
77/// would be alive simultaneously across the recursion.
78pub(crate) enum Visited<'a> {
79    /// A `(label, child)` map entry, fired once per label regardless of
80    /// whether `label`'s value is a same-label array -- e.g. for
81    /// `yaml.rs`'s NEL-in-label scan, which must not fire once per array
82    /// entry.
83    Edge { label: &'a str },
84    /// A value actually reached by the traversal (a leaf, or a
85    /// non-`Object` node on the way down) -- e.g. `json.rs`'s
86    /// NaN/Infinity check or `yaml.rs`'s NEL-in-value check, both of
87    /// which only care about a subset of node kinds and filter for it
88    /// themselves.
89    Node { value: &'a Value },
90}
91
92/// Shared traversal for the two codec scanners built directly over a grouped
93/// `Value` tree (`json::collect_leaves`/`check_json`, `yaml::scan_nel`) --
94/// see issue #51. Both re-implemented the same recursion and the same
95/// same-label-array path-numbering rule; this walker does it once.
96///
97/// `path` is a single reused buffer: every recursive step pushes its segment
98/// (via [`crate::report::push_child_path`]), recurses, then truncates back --
99/// so a full walk of an all-valid document allocates a path `String` only
100/// when `f` itself decides to keep one (e.g. to store it in a
101/// `WriteReport`), never once per edge just to *have* a path available
102/// (issue #44).
103///
104/// `toml.rs::strip_nulls` (which transforms the tree, not merely visits it)
105/// and `xml.rs::scan_xml_into` (a different tree type, `RawNode`, not
106/// `Value`) don't fit this shape and keep their own recursion -- they still
107/// use [`crate::report::child_path`] for the path-numbering rule itself.
108pub(crate) fn visit_grouped(
109    node: &Value,
110    path: &mut String,
111    f: &mut impl FnMut(Visited<'_>, &str),
112) {
113    match node {
114        Value::Object(map) => {
115            for (label, child) in map {
116                let base = path.len();
117                path.push('.');
118                path.push_str(label);
119                f(Visited::Edge { label }, path.as_str());
120
121                match child {
122                    Value::Array(items) => {
123                        path.truncate(base);
124                        for (i, item) in items.iter().enumerate() {
125                            let ibase = path.len();
126                            crate::report::push_child_path(path, label, i);
127                            visit_grouped(item, path, f);
128                            path.truncate(ibase);
129                        }
130                    }
131                    other => {
132                        visit_grouped(other, path, f);
133                        path.truncate(base);
134                    }
135                }
136            }
137        }
138        other => f(Visited::Node { value: other }, path.as_str()),
139    }
140}