Skip to main content

omnist/
oml.rs

1//! OML (Omnist Markup Language) -- the native codec for the Document model.
2//!
3//! Ported from `~/dev/omnist/omnist/oml.py` (issue #10). OML is omnist's own
4//! serialization format: every Document -- every ordered, possibly-repeated,
5//! possibly-interleaved edge list, and all seven scalar kinds (`string`,
6//! `integer`, `number`, `boolean`, `date`, `time`, `datetime`) plus `null` --
7//! round-trips through OML exactly, with no adjustment ever needed (unlike
8//! JSON/YAML/TOML/XML).
9//!
10//! This module implements the **OML-Core** grammar in full for both
11//! [`read_oml`] and [`write_oml`], plus the **OML-Extended** raw-string
12//! (`'...'`, E2) and triple-quoted multiline-string (`"""..."""`, E3)
13//! spellings on read only -- [`write_oml`] only ever emits OML-Core
14//! double-quoted strings, matching the Python reference.
15//!
16//! ## Layout (issue #53)
17//!
18//! `scanner` tokenizes source text, `parser` consumes those tokens into
19//! a [`RawNode`], and `writer` renders a `RawNode` back to OML-Core
20//! source. This top-level module keeps the module doc overview, the four
21//! `pub fn`s ([`read_oml`], [`write_oml`], [`write_oml_compact`],
22//! [`check_oml`]), and the `Codec` adapter --
23//! nothing about `crate::oml::*` paths changed by the split.
24//!
25//! ## Architecture (per issue #1/#10, "architecture freedom")
26//!
27//! Python's reader is a single-pass scanner built around one compiled
28//! "master" regex, deferring line/col computation and scalar-value
29//! construction until actually needed -- a Python-performance-specific
30//! design (see the module's PR #168 for the profile that motivated it), not
31//! a behavioral requirement. This port uses a straightforward hand-written
32//! recursive-descent scanner/parser over byte-indexed `&str` instead: idiomatic
33//! Rust, and there's no equivalent hot-path reason to defer decoding here.
34//! Observable behavior (parse results, round-trips, error content) matches
35//! the Python reference; exact error wording does not need to.
36//!
37//! ## Node representation
38//!
39//! [`crate::document::RawNode`] -- not [`crate::document::Value`] -- is the
40//! type this codec reads into and writes from. `Value::Object`'s `IndexMap`
41//! can't hold a repeated key, so it only represents "repeated label" as a
42//! *contiguous* run (an array value under one key); OML must round-trip
43//! arbitrary **interleaving** of repeated labels losslessly (its whole
44//! reason for existing -- "no adjustment ever needed"), which only
45//! `RawNode`'s literal edge list can hold exactly.
46//!
47//! ## Depth guard (omnist-ts#37 / omnist-ts#70)
48//!
49//! [`write_oml`] takes a plain, unchecked [`crate::document::RawNode`] --
50//! exactly like Python's `write_oml(node)`, which accepts any hand-built
51//! canonical node, not necessarily one that passed through a depth-checked
52//! builder. So the writer calls the shared
53//! `crate::document::check_write_depth` guard itself, at every nesting
54//! level, rather than assuming its input already got checked somewhere
55//! upstream -- the exact bug class omnist-ts#37/#70 were: a writer (or a
56//! second writer) that skipped this because *some* builder happened to
57//! guard depth already.
58
59#[cfg(test)]
60use crate::document::Scalar;
61use crate::document::{self, RawNode};
62use crate::error::{ParseError, WriteError};
63#[cfg(test)]
64use crate::formats::int_cap::MAX_INT_DIGITS;
65
66mod parser;
67mod scanner;
68mod writer;
69
70use parser::Parser;
71use scanner::Scanner;
72use writer::{write_edges, write_edges_compact, write_scalar};
73
74/// Parse OML source into a canonical [`RawNode`] (edge-list or leaf).
75///
76/// Supports the full OML-Core grammar, plus OML-Extended raw-string (`'...'`)
77/// and triple-quoted multiline-string (`"""..."""`) spellings -- see the
78/// module doc comment.
79pub fn read_oml(text: &str) -> Result<RawNode, ParseError> {
80    let sc = Scanner::new(text);
81    let mut parser = Parser::new(sc)?;
82    parser.parse_document()
83}
84
85/// Render a canonical [`RawNode`] as OML-Core source, pretty-printed with
86/// `indent` spaces per nesting level.
87///
88/// OML is lossless for every Document: there's never an adjustment to
89/// report, so there's no `strict=`/report machinery -- writing always
90/// succeeds, unless the input itself nests deeper than
91/// [`crate::document::MAX_DEPTH`] (see the module doc comment on the depth
92/// guard).
93pub fn write_oml(node: &RawNode, indent: usize) -> Result<String, WriteError> {
94    match node {
95        RawNode::Leaf(s) => Ok(write_scalar(s)),
96        RawNode::Edges(edges) => write_edges(edges, 0, indent, 0),
97    }
98}
99
100/// Single-line ("compact") rendering: edges joined by `"; "`, no
101/// newlines/padding. Mirrors Python's `write_oml(..., indent=None)`. Both
102/// forms round-trip through [`read_oml`].
103pub fn write_oml_compact(node: &RawNode) -> Result<String, WriteError> {
104    match node {
105        RawNode::Leaf(s) => Ok(write_scalar(s)),
106        RawNode::Edges(edges) => write_edges_compact(edges, 0),
107    }
108}
109
110/// Report what writing OML would adjust, without producing output. Added
111/// for issue #31 (the format registry): OML is lossless for every
112/// `Document` (see this module's doc comment), so there is never anything
113/// to report -- mirrors Python's `check_oml`, which is exactly `return
114/// WriteReport()`. Every other builtin format has a `check_*` function
115/// already; this is the OML counterpart, needed so the `"oml"` registry
116/// entry has a `check` callable like the other four.
117pub fn check_oml(_doc: &crate::document::Doc) -> crate::report::WriteReport {
118    crate::report::WriteReport::new()
119}
120
121/// Marker type implementing [`crate::formats::Codec`] for OML -- adapts
122/// [`read_oml`]/[`write_oml`]/[`check_oml`] to the registry's uniform
123/// `Doc`-in/`Doc`-out shape, exactly as `registry::builtins` did by hand
124/// before this issue: `read` bridges `read_oml`'s [`RawNode`] result
125/// through [`crate::document::Doc::from_raw`], and `write` bridges the
126/// other way through `Doc::to_raw` before calling `write_oml` with its
127/// documented default indent (2).
128pub(crate) struct Oml;
129
130impl crate::formats::Codec for Oml {
131    const NAME: &'static str = "oml";
132
133    fn read(text: &str) -> Result<document::Doc, crate::error::OmnistError> {
134        let raw: RawNode = read_oml(text)?;
135        document::Doc::from_raw(raw).map_err(Into::into)
136    }
137
138    fn write(doc: &document::Doc) -> Result<String, crate::error::OmnistError> {
139        write_oml(&doc.to_raw(), 2).map_err(Into::into)
140    }
141
142    fn check(doc: &document::Doc) -> crate::report::WriteReport {
143        check_oml(doc)
144    }
145}
146
147#[cfg(test)]
148mod tests;