moontoml

    TOML for MoonBit, 1.0.0 and earlier.

    toml
    toml-1.0
    toml-test
    toml-parser
    toml-encoder
    config
    configuration
    parser
    serializer
    decoder
    encoder
    datetime
    rfc3339
    json
    moonbit
    Download zip
    Version
    0.1.2
    License
    Apache-2.0
    Last updated
    13 hours ago
    Downloads
    982

    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

    #Two readings

    loads gives a Json tree: seven kinds of value, five of them in a form Json has. decode gives a tree of Value, which keeps every distinction the format makes. The difference is a date-time and a 64-bit integer, the two places where Json is narrower than TOML.

    TOMLloadsdecodeExample
    stringStringString(String)"loam"
    integerNumberInteger(Int64)42, 0x2A, 0o52, 0b101010
    floatNumberFloat(Real)1.5, 5e+22, inf, nan
    booleanTrue / FalseBoolean(Bool)true
    date-timeStringDatetime(Moment)1979-05-27T07:32:00Z
    arrayArrayArray(Array[Value])[1, 2, 3]
    tableObjectTable(Map[String, Value]){ x = 1 }

    Both readings accept the same documents and refuse the same ones; they differ only in what comes out.

    #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

    #Kinds, kept

    A Value is read by kind. A date is a Datetime and a string is a String, even when the string is written like a date:

    ///|
    fn main raise {
    let doc = @moontoml.decode("port = 12000\nwhen = 1979-05-27\nname = \"1979-05-27\"\n")
    guard doc is Table(fields) else { abort("a document is a table") }
    guard fields.get("when") is Some(Datetime(m)) else { abort("a date") }
    println(m.to_string())
    println(@moontoml.encode(doc))
    }

    1979-05-27 port = 12000 when = 1979-05-27 name = "1979-05-27"

    That is the whole point of the second reading: encode writes a String quoted and a Datetime bare, so a value keeps the kind it had, and no guess is made from the spelling.

    The four date-time kinds of the format are the four of Moment, which is moondate's type: an offset date-time, a local date-time, a local date and a local time. Integer holds all 64 bits, so a value past what a double holds exactly comes back as itself.

    #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.

    ///|
    fn main raise {
    let doc = @moontoml.loads("b = 1\na = 2\n"[:])
    println(@moontoml.dumps(doc)) // b = 1, then a = 2
    println(@moontoml.dumps(doc, sort=true)) // a = 2, then 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.

    dumps starts from a Json tree, which cannot say whether a string was a date, so it leans on the spelling: a string that is a date-time is written as one. encode starts from a Value and needs no such reading.

    #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 of one base, a float missing a digit on either side of its dot, a trailing comma in an inline table, a control character anywhere but a tab or a line break, and a date-time that is not spelled the way the format spells one — 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 each reading does about them.

    Dates become text. loads reads a date-time as its RFC 3339 form, and writes it back bare rather than quoted, so a timestamp stays a timestamp. decode reads it as a Datetime, which is the four kinds of the format. A date that does not exist, like 1979-02-30, is refused by both.

    Integers keep their digits. decode holds an integer in 64 bits, so every value the format allows survives exactly. loads has only a double, which is exact to 2^53, so the digits a number was written as travel beside the value whenever RFC 8259 spells it the same way, and 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.

    #Conformance

    The reader and the writer are checked against toml-test v2.2.0, the format's own suite, over the files it lists for TOML 1.0.0:

    SuiteDocumentsPassing
    valid, read as the tree the suite expects205205
    valid, written by the library and read back205205
    invalid, refused474474

    Nothing is skipped, and no case is expected to fail. The corpus is generated into the test sources by scripts/gen_vectors.py, which pins the release and checks its digest, so the numbers above are the ones the tests assert on every backend.

    #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

    Real

    pub(all) struct Real {
    value : Double
    repr : String?
    } derive(
    Debug
    )

    A float, and the text it was written as.

    TOML spells one number many ways — 1e06, 1000000.0, 1_000_000.0 — and a document read and written again should not turn one spelling into another. The text is kept whenever the source had one and it is also a JSON spelling, since that is what a tree of Json can carry; a float built in code has none and is written from its value.
    impl Eq for Real

    Real::equal

    fn Real::equal(self : Real, other : Real) -> Bool

    Real::new

    fn Real::new(value : Double) -> Real

    A float from its value alone.

    Real::not_equal

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

    Real::to_repr

    Value

    pub(all) enum Value {
    String(String)
    Integer(Int64)
    Float(Real)
    Boolean(Bool)
    Datetime(
    Moment
    )
    Array(Array[Value])
    Table(Map[String, Value])
    } derive(
    Debug
    )

    A TOML value, kind for kind.

    A Json tree has no date-time and only one number, so loads reads a timestamp into text and a 64-bit integer into a double. decode reads the same document and keeps what the format says it is, which is what writing it back without guessing needs.
    impl Eq for Value
    impl ToJson for Value

    Value::array

    fn Value::array(items : Array[Value]) -> Value

    An array value.

    Value::boolean

    fn Value::boolean(flag : Bool) -> Value

    A boolean value.

    Value::equal

    fn Value::equal(self : Value, other : Value) -> Bool

    Value::integer

    fn Value::integer(whole : Int64) -> Value

    An integer value.

    Value::not_equal

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

    Value::string

    fn Value::string(text : String) -> Value

    A string value.

    Value::table

    fn Value::table(fields : Map[String, Value]) -> Value

    A table value, which is what a document is.

    Value::to_json

    Value::to_repr

    decode

    fn decode(src : StringView, flavor? : Flavor) -> Value raise Refused

    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.

    decode_bytes

    fn decode_bytes(src : BytesView, flavor? : Flavor, bom? : Bool) -> Value 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.

    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.

    A Json tree says "string" for what may have been a date-time, so this leans on the spelling: a string that is a date-time is written as one. encode writes a Value and needs no such guess.

    encode

    fn encode(value : Value, sort? : Bool) -> String

    Write a tree of Value as a TOML document.

    Each value keeps the kind it has: a String is quoted even when it reads like a date, an Integer is written as the 64 bits it holds, and a Datetime is written bare, because turning it into a quoted string would change what it is.

    encode_bytes

    fn encode_bytes(value : Value, sort? : Bool) -> Bytes

    The same, as UTF-8 bytes.

    load

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

    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.

    loads

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

    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.

    strict

    let strict : Flavor

    The preset.