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}