Expand description
TOML codec. Ported from ~/dev/omnist/omnist/formats.py’s
read_toml/write_toml/check_toml.
§Crate choice
Like yaml.rs (issue #18), this module delegates raw tokenization to a
well-tested crate – [toml_edit] – rather than hand-rolling TOML’s
grammar (tables, dotted keys, inline tables, array-of-tables headers,
four temporal literal forms, three integer radixes). toml_edit is used
read-side only: read_toml walks its parsed DocumentMut into
this crate’s canonical crate::document::Value. The writer is
hand-written (mirroring json.rs/yaml.rs’s writers), producing TOML
text directly from a crate::document::Value rather than round-
tripping back through toml_edit’s own formatter – see “Writer output
shape” below for why.
Everything omnist-specific stays hand-written Rust, not delegated to the crate:
- Null-adjustment (
strip_nulls) – TOML has nonullat all; dropping a null-valued field/array-item and recording it viacrate::report::WriteReportis this module’s own logic (see “No null” below). - Temporal canonicalization (
format_datetime) – turningtoml_edit’s parsedDate/Time/Datetimestructs into this port’s canonical ISO-string spelling (see “Native temporal types” below). - Integer digit cap –
toml_edititself rejects any integer literal that doesn’t fit ini64with a generic “integer number overflowed” parse error that discards the offending literal’s text; this module recovers the raw digit run from the error’s byte span and re-derives the same 4300-digit-cap-vs-genuine-overflow distinctionjson.rs/yaml.rsalready make (see “Integer digit cap” below for why this needed hand-written recovery rather than being free from the crate). - Depth guard, shape-check reuse – see their own sections below.
§No null (the one unrepresentable-value case TOML has)
Spec Sec8.3.8/Sec8.3.9 (updated 2026-08-24): writing a node containing a
null-valued field now fails the write unconditionally
(write.unsupported-value, via [crate::report::unsupported_value_error])
regardless of strict, rather than the old “drop the field and record a
warning” behavior. TOML has no null token at all – dropping the edge
didn’t just alter what’s represented at that position, it erased the
edge’s existence entirely, with zero trace on read-back that a labeled
edge with that path was ever there (confirmed live; retired the
null.omitted code – see strip_nulls’s doc comment). check_toml
still reports the same condition as a write.unsupported-value
Severity::Error adjustment for preview purposes, since it never
produces output to begin with.
If stripping nulls leaves a document whose root isn’t a table (object),
write_toml raises WriteError unconditionally – not part of
the report, mirrors Python’s write_toml raising
WriteError("TOML needs a top-level table (the root must be an object)") outside of finish_write entirely (so it fires even when
report is supplied and even though the message never enters the
accumulated WriteReport).
§Integer digit cap (omnist-ts#54 / json.rs / yaml.rs precedent) –
not a natural 64-bit range check
The issue’s own framing floated “TOML integers are spec’d as 64-bit, so
this may be a natural range check rather than the 4300-digit-cap
mechanism used elsewhere” – live-checked against tomllib (Python’s
stdlib TOML reader, which omnist.formats.read_toml wraps directly) and
found not to be the case: tomllib.loads("x = " + "9"*4300) parses
successfully to a full-precision Python int (no 64-bit truncation, no
error) – while tomllib.loads("x = " + "9"*4301) raises ValueError: Exceeds the limit (4300 digits) for integer string conversion, the
identical CPython int(str)-conversion guard read_json’s comment
documents (sys.set_int_max_str_digits), not a TOML-spec-mandated
64-bit bounds check at all – for decimal literals only. Hex/octal/
binary literals are a genuine exception, not a false one: live-confirmed
tomllib.loads("x = 0x" + "f" * 5000) (and even 10000 fs) parses
successfully with no error at all, matching CPython’s own documented
carve-out (sys.set_int_max_str_digits explicitly exempts power-of-two
bases) – an earlier draft of this comment claimed hex/octal/binary hit
the identical digit-limit ValueError, which was wrong and has been
corrected. So Python’s TOML integer handling is json.rs’s/yaml.rs’s
existing 4300-digit-cap pattern for decimal literals, and uncapped for
hex/octal/binary.
This port does not replicate that decimal/non-decimal split: every
radix goes through the same toml_overflow_error recovery and the same
4300-digit cap, because toml_edit itself enforces a strict i64 range
at parse time regardless of radix (see below) – there is no path in this
implementation for an oversized hex/octal/binary literal to reach
Scalar::Int uncapped the way Python’s arbitrary-precision int does.
This is a disclosed divergence from Python, not parity: kept
deliberately rather than special-cased away, because (a) TOML 1.0 itself
specifies 64-bit signed integers and toml_edit enforces 64-bit bounds
at parse time, so an “uncapped hex” path would still fail for anything
over i64::MAX, and (b) capping the digit run uniformly preserves the
same superlinear-conversion DoS protection json.rs/yaml.rs apply,
without carving out a radix-specific exemption.
Where this module’s implementation had to diverge from json.rs’s
straight-line reuse: toml_edit itself enforces a strict 64-bit
range at parse time ("9"*20 / i64::MAX + 1 both fail with a
generic “integer number overflowed” TomlError that does not expose the
original digit run), which is stricter than Python’s real behavior for
anything between 20 and 4300 digits. To keep this port’s observable
integer-literal error behavior matching Python’s (not toml_edit’s
internal, incidentally-stricter parse limit), toml_overflow_error
recovers the raw literal text from the failed parse’s byte span
(TomlError::span) and re-derives the digit count itself, producing the
same two-tier message json.rs/yaml.rs give (“exceeds the 4300-digit
cap” vs “out of range for a 64-bit integer”) instead of surfacing
toml_edit’s own message directly.
§Native temporal types (the opposite direction from JSON’s problem)
Unlike JSON (no temporal type at all) and like YAML (a native but looser
timestamp grammar), TOML has four first-class temporal literal forms
(local date, local time, local datetime, offset datetime) that are
stricter-shaped than YAML’s – toml_edit’s own parser already fully
validates calendar/clock fields (leap years, per-month day counts, valid
hour/minute/offset ranges: live-confirmed 2024-02-30, 2024-13-01,
25:00:00, 00:60:00, and a +25:00 offset are all parse
errors, not accepted-then-rejected-later), so read_toml does not
need to re-validate calendar/clock fields the way yaml.rs’s
normalize_timestamp must (YAML’s own crate does no such validation).
crate::document::Scalar has real Date/Time/Datetime variants
(issue #105) – toml_value_to_value reads toml_edit’s own already
fully-validated Datetime struct’s date/time presence to construct
the right one directly (real provenance, not a shape guess), and
format_datetime renders the canonical ISO spelling either way:
zero-padded, T-joined, a bare Z offset normalized to +00:00
(matching yaml.rs’s identical normalization – this port’s canonical
temporal strings never contain a literal Z, only a numeric offset,
which is what crate::schema::is_iso_datetime’s regex expects).
Fractional seconds beyond microsecond precision are truncated, not
rounded, to six digits – live-confirmed against tomllib:
00:32:00.9999999 (7 nines) reads as datetime.time(0, 32, 0, 999999)
(truncated, not rounded to 1000000 and carried), matching this
module’s nanosecond / 1000 integer-truncating conversion exactly.
UTC-offset preservation (the omnist-ts#51-pattern check this issue
calls for): an offset datetime’s numeric offset is preserved exactly in
the canonical string (-07:00 stays -07:00across read+write), so this
module does not repeat the OML writer’s offset-erasure bug – confirmed
by this module’s round_trips_offset_datetime_preserving_negative_offset
and _positive_offset tests.
On the write side, a genuine Scalar::Date/Datetime writes as a
native TOML temporal literal (unquoted) unconditionally – no
shape-check, since the variant itself is the provenance signal (issue
#105, the same fix issue #99 already applied to OML). An ordinary
Scalar::Str always writes quoted, however date-shaped its text –
this now matches Python’s write_toml exactly (previously diverged:
Python’s document model retains a real datetime.date/time/
datetime object end-to-end, so a plain str that merely looks like a
date – live-confirmed: tomli_w.dumps({'a': '1979-05-27'}) – always
wrote as the quoted string a = "1979-05-27", never a native literal;
this port’s pre-#105 Scalar had no way to make that distinction, so
it wrote bare unconditionally whenever the text merely looked
temporal-shaped – a real bug, confirmed live and fixed, not a
permitted variation). A Scalar::Time carrying a UTC offset is the one
remaining case with no native TOML spelling at all (TOML’s local
time literal has no offset field) – see write_scalar’s own
has_offset fallback.
§Depth guard reuse
read_toml parses TOML text into a crate::document::Value, then
builds a Doc via Doc::of – which calls
crate::document::check_write_depth internally (see document.rs).
write_toml/check_toml/strip_nulls walk an already-built
Doc (via Doc::to_grouped), whose every node was depth-checked at
construction time – exactly json.rs’s reasoning (not yaml.rs’s,
which pre-processes raw structures before Doc::of ever runs) –
there is nothing left to re-guard on the way out, so strip_nulls does
not take or check a depth parameter at all.
toml_edit additionally enforces its own, separate recursion cap
while parsing – empirically found (see this module’s tests) to reject
TOML text nested roughly 81 levels deep (inline tables), well below this
crate’s own 200-level MAX_DEPTH. This means a read-side test can
only ever observe toml_edit’s own ParseError firing first, never
this crate’s DocumentError – the depth-guard-reuse obligation is
instead demonstrated the way json.rs/yaml.rs already do, by building
an over-deep Value directly and confirming Doc::of rejects it
(see deeply_nested_document_write_reuses_doc_construction_depth_guard).
§Writer output shape (architecture freedom, per issue #1)
This module always emits nested tables and table-arrays as inline
TOML ({ k = v } / [ v, v ]), never [section]/[[section]] headers.
This is a deliberate divergence from tomli_w’s (and most hand-written
TOML’s) header-based style, chosen because it is unambiguous, needs no
header-nesting state machine, and is fully spec-valid TOML – the “one
constraint” from issue #1 is observable behavior (what a round trip
produces), not byte-for-byte resemblance to tomli_w’s pretty-printing
choices, and inline tables/arrays parse back to an identical Doc
either way.
Functions§
- check_
toml - Report what writing TOML would adjust, without producing output.
- read_
toml - Parse TOML text into a
Doc. - write_
toml - Project a
Docto TOML text. See this module’s doc comment for the null-adjustment, integer-cap, temporal, and output-shape decisions.