// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// Where in the source something is: the offset in code units, and the line and
/// column a person counts from one.
#derive(Eq, Debug)
pub(all) struct At {
at : Int32
line : Int32
column : Int32
}
///|
pub impl Show for At with fn output(self, logger) {
logger.write_string("\{self.line}:\{self.column}")
}
///|
pub extend At with Debug::{to_repr}
///|
pub extend At with Show::{to_string, output}
///|
pub extend At with Eq::{not_equal, equal}
///|
/// Why a document was refused.
///
/// `Redefined` is the rule that matters most: §1 says a key may be defined only
/// once, and so may a table, so a second definition is an error rather than a
/// value that wins. It is the rule that makes a TOML file safe to append to.
#derive(Eq, Debug)
pub(all) suberror Refused {
Unexpected(At, Char)
Truncated(At)
BadEscape(At)
BadNumber(At)
BadDate(At)
Redefined(At, String)
MixedArray(At)
Trailing(At)
TooDeep(At)
BadUtf8(At)
}
///|
/// A refusal prints as the fault and the line it is on.
pub impl Show for Refused with fn output(self, logger) {
match self {
Unexpected(at, c) => logger.write_string("\{at}: unexpected '\{c}'")
Truncated(at) => logger.write_string("\{at}: ends mid-document")
BadEscape(at) => logger.write_string("\{at}: not an escape")
BadNumber(at) => logger.write_string("\{at}: not a number")
BadDate(at) => logger.write_string("\{at}: not a date or time")
Redefined(at, k) => logger.write_string("\{at}: \{k} is already defined")
MixedArray(at) =>
logger.write_string("\{at}: an array is one kind before 1.0")
Trailing(at) => logger.write_string("\{at}: trailing content")
TooDeep(at) => logger.write_string("\{at}: nested too deep")
BadUtf8(at) => logger.write_string("\{at}: not UTF-8")
}
}
///|
pub extend Refused with Debug::{to_repr}
///|
pub extend Refused with Show::{to_string, output}
///|
pub extend Refused with Eq::{not_equal, equal}
///|
/// Where the refusal happened.
///
/// Every variant carries a position, so a caller reporting a line does not have to
/// match all ten of them to get at it.
pub fn Refused::at(self : Refused) -> At {
match self {
Unexpected(at, _)
| Truncated(at)
| BadEscape(at)
| BadNumber(at)
| BadDate(at)
| Redefined(at, _)
| MixedArray(at)
| Trailing(at)
| TooDeep(at)
| BadUtf8(at) => at
}
}
///|
/// Which edition of the format a document is read under.
///
/// The editions differ by real grammar: `v04` has no dotted keys, no
/// hex/octal/binary integers and no `inf`/`nan`; `v05` adds those plus the three
/// local date-time kinds; `v100` is the current strict reading, and `v110` relaxes
/// inline tables to span lines with a trailing comma, adds the `\x` and `\e`
/// escapes and lets a time drop its seconds.
#derive(Eq, Debug)
pub(all) enum Edition {
V04
V05
V100
V110
}
///|
pub extend Edition with Debug::{to_repr}
///|
pub extend Edition with Eq::{not_equal, equal}
///|
/// How a document is read.
///
/// The preset is `strict`: TOML 1.0.0 and a nesting limit of 500. Build another
/// with `Flavor::new`, or update one in place with `{ ..strict, depth: 64 }`. The
/// version-specific presets live in the `v04`, `v05` and `v110` packages.
///
/// There is no knob for a repeated key. TOML 1.0.0 §1 makes redefinition an error,
/// not a policy, so making it configurable would be offering to read a document the
/// format says is not one.
#derive(Eq, Debug)
pub(all) struct Flavor {
depth : Int32
edition : Edition
}
///|
pub extend Flavor with Debug::{to_repr}
///|
pub extend Flavor with Eq::{not_equal, equal}
///|
/// How deep a document may nest.
///
/// 500 is the preset: deep enough for real configuration, shallow enough that a
/// hostile document cannot run the stack out.
pub let depth : Int32 = 500
///|
/// The preset: TOML 1.0.0 at the preset depth. The `v04`, `v05` and `v110`
/// packages each fix a different edition and expose the same entry points.
pub let strict : Flavor = { depth, edition: V100, }
///|
/// A flavor by name, every knob with the preset's value.
pub fn Flavor::new(depth~ : Int32 = depth, edition~ : Edition = V100) -> Flavor {
{ depth, edition, }
}
///|
/// The document in `src`, kind for kind.
///
/// TOML defines one document rather than a stream, so there is no plural form of
/// this. This is the reading that keeps every distinction the format makes: a date
/// is a date and not text, and an integer is 64 bits and not a double.
pub fn decode(
src : StringView,
flavor~ : Flavor = strict,
) -> Value raise Refused {
check_text(src)
Reader::new(src, flavor).document()
}
///|
/// The document in `src`, kept as it was written.
///
/// This is the reading for a file a person edits: change a value in place and write
/// the result back, and the rest of the document — the comments, the blank lines,
/// the order, the spelling of every value but the one that moved — is the source
/// byte for byte. See `Doc`.
pub fn decode_doc(
src : StringView,
flavor~ : Flavor = strict,
) -> Doc raise Refused {
Doc::decode(src, flavor~)
}
///|
/// The document in `src`, decoded as UTF-8.
///
/// TOML 1.0.0 §1 says a file is UTF-8 and does not allow a byte-order mark; `bom`
/// skips one anyway, because an editor on Windows writes it and refusing the file
/// helps nobody. Pass `bom=false` to hold the format to its word.
pub fn decode_bytes(
src : BytesView,
flavor~ : Flavor = strict,
bom~ : Bool = true,
) -> Value raise Refused {
let text = @utf8.decode(src, ignore_bom=bom) catch {
_ => raise BadUtf8({ at: 0, line: 1, column: 1, })
}
decode(text, flavor~)
}
///|
/// The document in `src`, as a tree of `Json`.
///
/// A date-time comes out as its RFC 3339 text and an integer as a `Json` number,
/// because a `Json` tree has no place to keep either apart; `decode` is the reading
/// that keeps them.
pub fn loads(src : StringView, flavor~ : Flavor = strict) -> Json raise Refused {
decode(src, flavor~).to_json()
}
///|
/// The document in `src`, decoded as UTF-8, as a tree of `Json`.
///
/// TOML 1.0.0 §1 says a file is UTF-8 and does not allow a byte-order mark; `bom`
/// skips one anyway, because an editor on Windows writes it and refusing the file
/// helps nobody. Pass `bom=false` to hold the format to its word.
pub fn load(
src : BytesView,
flavor~ : Flavor = strict,
bom~ : Bool = true,
) -> Json raise Refused {
decode_bytes(src, flavor~, bom~).to_json()
}