moontoml

    TOML for MoonBit, 1.0.0 and earlier.

    toml
    toml-1.0
    config
    configuration
    parser
    serializer
    moonbit
    Download zip
    Version
    0.1.1
    License
    Apache-2.0
    Last updated
    2 days ago
    Downloads
    679

    moontoml


    moon add moonbitstack/moontoml

    A document goes in, a tree comes out. Write the tree back and the values are the same.

    ///|
    fn main raise {
    let src =
    #|title = "field notes"
    #|beds = 4
    #|
    #|[soil]
    #|kind = "loam"
    #|ph = 6.5

    let doc = @moontoml.loads(src[:])
    println(@moontoml.dumps(doc))
    }

    title = "field notes" beds = 4 [soil] kind = "loam" ph = 6.5

    #What a document becomes

    TOML has seven kinds of value, and each one has a place in a Json tree:

    A date comes out as the RFC 3339 form, not the text it went in as: 2026-03-01 09:30:00Z becomes 2026-03-01T09:30:00Z. A date the calendar does not have is refused.

    #Reading values

    A Json is read by shape. Object is a table, String a string, and a missing key is None.

    ///|
    fn main raise {
    let src =
    #|title = "field notes"
    #|beds = 4
    let doc = @moontoml.loads(src[:])
    guard doc is Object(root) else { abort("a document is a table") }
    let title = match root.get("title") {
    Some(String(s)) => s
    _ => "?"
    }
    let beds = match root.get("beds") {
    Some(Number(n, ..)) => n.to_int()
    _ => 0
    }
    println("\{title}, \{beds} beds")
    }

    field notes, 4 beds

    #Writing it back

    dumps writes scalars at the level they are on and puts tables under their own headers. The order is the tree's own; sort=true orders keys by name.

    let doc = @moontoml.loads("b = 1\na = 2\n"[:])
    println(@moontoml.dumps(doc)) // b = 1
    // a = 2
    println(@moontoml.dumps(doc, sort=true)) // a = 2
    // b = 1

    A document read and written again keeps its values, not its layout. TOML can spell the same tree several ways; this writer picks one.

    #The interface

    #What it reads

    Everything TOML 1.0.0 defines: bare, quoted and dotted keys; [table] and [[array of tables]] headers; the four string forms with their escapes and the line-ending backslash; integers in decimal, hexadecimal, octal and binary with digit-grouping underscores; floats with exponents and inf/nan; the four date-time types; arrays that span lines; and inline tables.

    #What it refuses

    TOML's redefinition rules, which are what make a file safe to append to:

    • A key defined twice.
    • A table header repeated.
    • A dotted key later opened by a header.
    • A key added to an inline table after its brace closed.
    • [[…]] over something that is not an array of tables.

    Also a leading zero on a multi-digit integer, an underscore not between two digits, a float missing a digit on either side of its dot, and a trailing comma in an inline table — all of which TOML 1.0.0 forbids.

    Every refusal carries an At. The nine kinds are Unexpected, Truncated, BadEscape, BadNumber, BadDate, Redefined, Trailing, TooDeep and BadUtf8.

    #Dates and integers

    Two places where a Json tree is narrower than the format, and what is done about each.

    Dates become text. TOML has four date-time types and a tree of Json nodes has none. They are read as their RFC 3339 form, and written back bare rather than quoted, so a timestamp stays a timestamp and not a string. A date that does not exist, like 1979-02-30, is refused.

    Integers keep their digits. A Json number is a double, which is exact only to 2^53. The digits a number was written as travel beside the value whenever RFC 8259 spells it the same way, so 9007199254740993 writes back as itself. A spelling RFC 8259 does not share — +99, 0xff, 1_000, inf — carries no text and is written from the value.

    #Examples

    ExampleWhat it shows
    src/examples/basicthe four steps: load, read, edit, dump
    src/examples/integrationa site config read into a struct, changed, written back sorted, and a refusal reported by line

    moon run src/examples/basic moon run src/examples/integration

    #Licence

    Apache-2.0. See LICENSE.

    Refused

    pub(all) suberror Refused {
    Unexpected(At, Char)
    Truncated(At)
    BadEscape(At)
    BadNumber(At)
    BadDate(At)
    Redefined(At, String)
    Trailing(At)
    TooDeep(At)
    BadUtf8(At)
    } derive(Eq,
    Debug
    )

    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.
    impl Show for Refused

    Refused::at

    fn Refused::at(self : Refused) -> At

    Where the refusal happened.

    Every variant carries a position, so a caller reporting a line does not have to match all nine of them to get at it.

    Refused::equal

    fn Refused::equal(Refused, Refused) -> Bool

    Refused::not_equal

    fn Refused::not_equal(x : Refused, y : Refused) -> Bool

    Refused::output

    fn Refused::output(self : Refused, logger : &Logger) -> Unit

    Refused::to_repr

    Refused::to_string

    fn Refused::to_string(self : Refused) -> String

    pub(all) struct At {
    at : Int
    line : Int
    column : Int
    } derive(Eq,
    Debug
    )

    Where in the source something is: the offset in code units, and the line and column a person counts from one.
    impl Show for At

    At::equal

    fn At::equal(At, At) -> Bool

    At::not_equal

    fn At::not_equal(x : At, y : At) -> Bool

    At::output

    fn At::output(self : At, logger : &Logger) -> Unit

    At::to_repr

    At::to_string

    fn At::to_string(self : At) -> String

    Flavor

    pub(all) struct Flavor {
    depth : Int
    } derive(Eq,
    Debug
    )

    How a document is read.

    The preset is strict. Build another with Flavor::new, or update one in place with { ..strict, depth: 64 }.

    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.

    Flavor::equal

    fn Flavor::equal(Flavor, Flavor) -> Bool

    Flavor::new

    fn Flavor::new(depth? : Int) -> Flavor

    A flavor by name, every knob with the preset's value.

    Flavor::not_equal

    fn Flavor::not_equal(x : Flavor, y : Flavor) -> Bool

    Flavor::to_repr

    depth

    let depth : Int

    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.

    dump

    fn dump(value : Json, sort? : Bool) -> Bytes

    The same, as UTF-8 bytes.

    dumps

    fn dumps(value : Json, sort? : Bool) -> String

    Write a Json tree as a TOML document.

    Tables go under their own headers and scalars stay at the level they are on, so the output reads like a configuration file someone wrote rather than one a program emitted. An array of tables is written as [[…]] when every one of its elements is a table, and inline otherwise, which is the only honest choice: TOML has no way to spell an array mixing tables with scalars across headers.

    sort orders keys by name; off, they keep the order the tree has. A document read by this module and written again carries the same values, but not necessarily the same layout — TOML can spell the same tree several ways.

    load

    fn load(src : BytesView, flavor? : Flavor, bom? : Bool) -> Json raise Refused

    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.

    loads

    fn loads(src : StringView, flavor? : Flavor) -> Json raise Refused

    The document in src.

    TOML defines one document rather than a stream, so there is no plural form of this.

    strict

    let strict : Flavor

    The preset.