moonyaml

    YAML 1.0, 1.1 and 1.2 for MoonBit.

    yaml
    config
    moonbit
    Download zip
    Version
    0.1.3
    License
    Apache-2.0
    Last updated
    2 days ago
    Downloads
    1K

    #moonyaml

    YAML for MoonBit, in five versions and two shapes.

    A document can be read as a value — the Json tree every reader in this family produces — or as a document, a tree that keeps the comments, the anchors, the tags and the style it was written with, so it can be written back. Both come from one scanner: a version is a resolver, not a fork.

    test {
    let config = @moonyaml.loads(
    #|server:
    #| host: 0.0.0.0
    #| port: 12000
    #|limits: &shared { files: 1000, bytes: 1048576 }
    #|upload: *shared
    )
    println(@moonyaml.dumps(config)) // back to YAML, block style
    }

    test {
    let doc = @moonyaml.parse(
    #|# the deployment
    #|name: web
    #|ports: [80, 443]
    )
    println(@moonyaml.emit(doc)) // the same document, comment and all
    println(doc.to_json().stringify()) // what it means
    }

    #Install

    moon add moonbitstack/moonyaml

    #The packages

    PackageWhat it is
    moonbitstack/moonyamlYAML 1.2.2's core schema, the writer, the document model, and the typed codec
    moonbitstack/moonyaml/yaml11YAML 1.1: yes/no/on/off as booleans, 014 as octal, sexagesimals, 1_000, and the << merge key
    moonbitstack/moonyaml/yaml10YAML 1.0's types, which 1.1 inherited but did not always keep
    moonbitstack/moonyaml/strictThe strict dialect: block structure only, every scalar a string, no flow style, no anchors, no tags, no repeated keys
    moonbitstack/moonyaml/anyReads the version a document asks for with %YAML, and 1.2.2 when it asks for nothing

    They are packages rather than a flag because a version is a promise about meaning: yes is a boolean in one and a string in another, and a reader that decides which by looking at the file is a reader whose output a caller cannot type. any exists for the caller who is reading files they did not write, and it is the only package that guesses — from the document, once, at the top.

    #The interface

    FunctionGives back
    loads(src) / loads_all(src)Json — the one document, or every document in the stream
    load(bytes) / load_all(bytes)The same, decoding UTF-8 and skipping a leading BOM
    loads_as[T](src) / load_as[T](bytes)A typed value, through @json.FromJson
    dumps(value) / dumps_all(values)YAML text
    dump(value) / dump_all(values)The same, as Bytes
    dumps_of[T](value) / dumps_of_all[T](values)The same, through @json.ToJson
    round_trip[T](src, adjust~)A typed value written back, for checking that a decode is a decode
    parse(src) / parse_all(src)Node — the document, comments and all
    emit(node)YAML text that reads back as the same document
    events_all(nodes) / node.events()The parse, as the event stream a reader would report
    declared_version(src)The %YAML a document asks for, or None

    Each version package carries the same loads/loads_all/load/load_all/ dumps/dumps_all, so a caller can take one as a parameter and a program can hold a list of versions instead of a branch per version.

    #What it reads

    CollectionsBlock mappings and sequences, flow {} and [], nested in either direction, a sequence in its parent key's own column, a collection as a key
    ScalarsPlain over several lines (breaks fold, blank lines do not), single- and double-quoted with the escapes of §5.7, literal \| and folded > with chomping and explicit indentation
    StructureAnchors and aliases, tags including %TAG handles and verbatim !<…>, %YAML, --- and ... markers, ? key explicit pairs, complex keys
    ResolutionThe core schema of §10.2: null/~, booleans, decimal, 0o and 0x integers, floats with exponents, .inf and .nan

    yes and no are strings here, as YAML 1.2 says they are. They are booleans in 1.1, which is yaml11, and the difference is a package boundary rather than a flag: 014 is twelve there and fourteen here, and 1:30 is ninety there and a string here.

    #The document

    parse gives back a Node: a kind, the text a scalar was written as, the style of the quoting, whether a collection was flow, its anchor and tag, the comments before it and the one after it, and whether the document it heads was opened by an explicit --- or closed by a ....

    test {
    let doc = @moonyaml.parse(
    #|# the deployment
    #|name: web
    #|ports: [80, 443]
    #|motd: |
    #| one
    #| two
    )
    match doc.kind {
    Mapping(pairs) => println(pairs[2].0.text) // ports
    _ => ()
    }
    println(@moonyaml.emit(doc)) // the same file, comment and all
    }

    A mapping is an Array[(Node, Node)] rather than a map: order is part of a document, a key is a node rather than a string, and a file may legitimately have a sequence as a key. An alias stays an alias, so emit writes *shared instead of the lines it stands for, and a scalar keeps the spelling it had — 014 is written back as 014.

    Node::events reports the parse instead of the value: one line per event, the notation the YAML Test Suite uses for a parse tree. It is what a caller compares with when two documents should be the same document rather than the same value — after an edit that was supposed to touch nothing, say.

    +STR +DOC +MAP =VAL :name =VAL :web -MAP -DOC -STR

    #Configuration

    Two records, each with presets, each field overridable, and every knob reachable as a named argument on the call:

    @moonyaml.loads(src, flavor=@moonyaml.Flavor::new(duplicates=Reject))
    @moonyaml.loads(src, flavor={ ..@moonyaml.strict, budget: 1000 })
    @moonyaml.dumps(value, style=@moonyaml.flow)
    @moonyaml.dumps(value, indent=4, sort=true)

    A flat argument wins over a field of the record, which wins over the preset's value. There is no other way to change a default, and no default is hidden.

    FlavorPresetWhy that value
    depth500Deeper than any configuration file nests, shallow enough that the reader's own recursion cannot be driven off the stack
    duplicatesLastA mapping names each key once (§3.2.1.1); where one is written twice, the last is the one the document ends with. First and Reject are there because a repeated key is usually a mistake
    aliasestrueAn alias is part of the language; false refuses one outright
    budget10 000 000Nodes one document's aliases may expand to. A dozen lines of aliases can name billions of nodes, so the expansion is counted and capped rather than performed

    StylePresetWhy that value
    indent2Two columns per level, which is what a hand-written configuration file uses and what fits a diff
    width80Where a collection stops fitting on a line and Auto breaks it
    layoutBlockBlock style reads better in a diff; Flow writes JSON's own shape, Auto decides per collection by width
    quotesMinimalQuote only what has to be quoted, which is what a person writing YAML does
    anchorstrueA collection that appears twice is written once and referenced
    sortfalseA document keeps the order it was written in; a caller who wants a stable file turns this on

    dumps_all opens every document with its own ---, which is the convention that stays correct when streams are concatenated. emit puts back the markers the document it was given carried, and nothing else.

    #What it refuses

    Everything it cannot resolve to one reading. YAML's own answer to ambiguity is to be permissive, which is how a configuration file comes to mean something other than what it looks like; this reader raises Refused with a line and a column instead:

    a: [1, 2 => 1:4: ends mid-document a: *nope => 1:4: no anchor named `nope` a: 1, a: 2 => 2:1: the key `a` is written twice

    A mapping key that is itself a collection is refused by loads, because the tree it produces has string keys and there is no honest way to spell a sequence as one — parse keeps it, since a document has keys that are not strings.

    A tag from a vocabulary this reader does not have — a local !foo, a !!python/object, an unregistered tag:example.com,2000:app/foo — is not one of those. §10.4 permits a processor to resolve such a node as if it carried no tag, and that is what happens: the scalar keeps the type its text has, so !!nope 1 is the number 1. Refusing the file instead would make it unreadable, and inventing a type for it would be worse. parse keeps the tag on the node, so a document can be written back with it intact.

    #Examples

    moon run src/examples/tour # everything it reads, in one file moon run src/examples/versions # one document, read five ways moon run src/examples/document # the document model, and a round trip

    #Licence

    Apache-2.0. See LICENSE.

    Malformed

    Why a document did not become the value the caller asked for.

    Two things can go wrong, and they are different things: the YAML may not have been read at all, or it may have been read and not be the shape a type needs. Both carry where — the reader's position, or the path into the document — so a caller can say which line to look at.
    impl Show for Malformed

    Malformed::output

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

    Malformed::to_string

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

    Refused

    pub(all) suberror Refused {
    Unexpected(At, Char)
    Truncated(At)
    BadEscape(At)
    BadIndent(At)
    BadTag(At, String)
    BadAnchor(At, String)
    Repeated(At, String)
    Trailing(At)
    TooDeep(At)
    Exceeded(at~ : At, limit~ : Int, got~ : Int)
    BadUtf8(At)
    Disallowed(At, String)
    } derive(Eq,
    Debug
    )

    Why a document was refused.

    YAML's own answer to ambiguity is to be permissive, which is how a configuration file comes to mean something other than what it looks like. This reader refuses instead: anything it cannot resolve to one reading is an error with a position, never a guess.
    impl Show for Refused

    Refused::at

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

    Where the refusal happened.

    Every variant carries a position, so a caller that wants to report the line and column does not have to match all twelve 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.

    Every refusal carries one, because a configuration file that will not load is read by a person who needs to know which line to look at.
    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

    Duplicates

    pub(all) enum Duplicates {
    Last
    First
    Reject
    } derive(Eq,
    Debug
    )

    What to do when a mapping names the same key twice.

    YAML 1.2.2 §3.2.1.1 says the keys of a mapping are unique and leaves the handling of a duplicate to the processor. Every loader in reach takes the last one, which is what Last does; First and Reject are here because a configuration file with a key written twice is usually a mistake someone would rather be told about.

    Duplicates::equal

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

    Duplicates::not_equal

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

    Flavor

    pub(all) struct Flavor {
    depth : Int
    duplicates : Duplicates
    aliases : Bool
    budget : Int
    } derive(Eq,
    Debug
    )

    How a document is read: the limits, and what to do with a duplicate key.

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

    Flavor::equal

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

    Flavor::new

    fn Flavor::new(depth? : Int, duplicates? : Duplicates, aliases? : Bool, budget? : 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

    Kind

    pub(all) enum Kind {
    Null
    Bool(Bool)
    Number(Double)
    Str(String)
    Sequence(Array[Node])
    Mapping(Array[(Node, Node)])
    Alias(String)
    }

    What a node is.

    An alias is a node of its own rather than a copy of what it names: a document with an anchor and three aliases is three references, and writing it back as twelve copies would be a different document.

    Layout

    pub(all) enum Layout {
    Block
    Flow
    Auto
    } derive(Eq,
    Debug
    )

    How a collection is laid out.

    Block is the style a configuration file is written in: one entry per line, a person can read it and a diff shows one change per line. Flow is the brace and bracket form, which JSON shares and which a machine-generated file often wants. Auto picks per collection: flow when it holds nothing but scalars and the whole of it fits in [Style::width], block otherwise. A collection of collections stays block, because that is the shape of a configuration file.

    Layout::equal

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

    Layout::not_equal

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

    Layout::to_repr

    Node

    pub(all) struct Node {
    kind : Kind
    text : String
    style : ScalarStyle
    flow : Bool
    anchor : String?
    tag : String?
    before : Array[String]
    after : String?
    explicit : Bool
    closed : Bool
    }

    One node of a document.

    Node::anchored

    fn Node::anchored(self : Node, name : String) -> Node

    This node, anchored.

    Node::commented

    fn Node::commented(self : Node, before~ : Array[String], after~ : String) -> Node

    This node, with the comments written around it.

    Node::events

    fn Node::events(self : Node) -> String

    One document as the events a parser produces for it: +DOC down to -DOC, with a +MAP/+SEQ for a collection, =VAL for a scalar and =ALI for a reference, each nested one space further in than the event that opened it.

    The notation is the YAML Test Suite's, which is where it is specified: the style a scalar was written in is the character before its text (:, ', ", |, >), a flow collection carries {} or [], an anchor &name and a tag <tag> come before the value, and control characters are escaped.

    This is the document as a parse rather than as a value: a key that is not a string, a key written twice, two documents in one stream — all of them have events, and none of them has a Json. A caller who wants to know whether two documents are the same document rather than the same value can compare these.

    Node::in_flow

    fn Node::in_flow(self : Node, flow : Bool) -> Node

    This node, written in flow style.

    Node::map

    fn Node::map(pairs : Array[(Node, Node)]) -> Node

    A block mapping.

    Node::of_json

    fn Node::of_json(value : Json) -> Node

    A tree for a Json value: the document a program would write for it.

    Node::of_kind

    fn Node::of_kind(kind : Kind, text : String) -> Node

    A scalar of a known kind, keeping the spelling it was read from.

    Node::same

    fn Node::same(self : Node, other : Node) -> Bool

    Whether two documents would be written the same way.

    The comparison a round trip is: read, write, read again, and the second tree is the first one. Positions are not in the tree, so this is equality of content.

    Node::scalar

    fn Node::scalar(text : String) -> Node

    A plain scalar, which is how most of YAML is written.

    Node::seq

    fn Node::seq(items : Array[Node]) -> Node

    A block sequence.

    Node::styled

    fn Node::styled(self : Node, style : ScalarStyle) -> Node

    This node, written in a particular scalar style.

    Node::tagged

    fn Node::tagged(self : Node, tag : String) -> Node

    This node, tagged.

    Node::to_json

    fn Node::to_json(self : Node) -> Json

    The Json this document denotes, with its aliases expanded.

    Quotes

    pub(all) enum Quotes {
    Minimal
    Single
    Double
    } derive(Eq,
    Debug
    )

    Which quote a string gets when it needs one.

    Minimal quotes only what would otherwise read as something else, which is what makes the output look written rather than generated. Single and Double are for a caller whose output feeds a second reader that is happier with one form.

    Quotes::equal

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

    Quotes::not_equal

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

    Quotes::to_repr

    Rules

    pub(all) struct Rules {
    yes_no : Bool
    letters : Bool
    octal : Bool
    binary : Bool
    underscores : Bool
    sexagesimal : Bool
    strings : Bool
    } derive(Eq,
    Debug
    )

    What a YAML version resolves an unquoted scalar to.

    The three released versions disagree with each other, and the disagreements are all here rather than in the scanner: 1.1 reads yes as a boolean, 014 as twelve and 1:30 as ninety, 1.2 reads all three as strings, and StrictYAML reads every scalar as a string whatever it looks like. A version is a set of rules over one reader, which is why this library has one parser and not four.

    A dialect nobody has published yet — a configuration language of your own, a pre-1.0 variant — is a Rules of your own, passed through [Schema]. Nothing here is closed.

    Rules::equal

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

    Rules::not_equal

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

    Rules::to_repr

    ScalarStyle

    pub(all) enum ScalarStyle {
    Plain
    Single
    Double
    Literal(Int)
    Folded(Int)
    } derive(Eq)

    How a scalar was written.

    A block scalar carries the chomping indicator that was written with it, because | and |- are different strings at the end of a line.

    ScalarStyle::equal

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

    ScalarStyle::not_equal

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

    Schema

    pub(all) struct Schema {
    resolve : (String, Bool, String?, At, Rules) -> Json raise Refused
    rules : Rules
    merge : Bool
    tags : Bool
    anchors : Bool
    flow : Bool
    secondary : String
    primary : String
    }

    What a YAML version does that the scanner cannot decide for it.

    The scanner in this package is shared; what a version resolves a scalar to, and what it allows besides, is a Schema.

    The one in this package is [core], the core schema of §10.2. The v11, v10, strict and any packages beside it are each a schema over the same scanner, which is why a version is a resolver rather than a fork of the reader.

    Style

    pub(all) struct Style {
    indent : Int
    width : Int
    layout : Layout
    quotes : Quotes
    anchors : Bool
    sort : Bool
    } derive(Eq,
    Debug
    )

    How a document is written: the indentation, the line width, the layout, the quotes, and whether a repeated subtree is written once and referred to after.

    The preset is [style]. Build another with [Style::new], or update one in place with { ..style, width: 120 }.

    Style::equal

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

    Style::new

    fn Style::new(indent? : Int, width? : Int, layout? : Layout, quotes? : Quotes, anchors? : Bool, sort? : Bool) -> Style

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

    Style::not_equal

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

    Style::to_repr

    ancient_rules

    let ancient_rules : Rules

    YAML 1.0's, which is 1.1 plus the single letters y and n. The 1.1 revision removed them because a lone n is more often a name than a no.

    budget

    let budget : Int

    How many nodes the aliases in one document may expand to.

    An alias can name a node that itself contains aliases, so a dozen lines can expand to billions of nodes: the "billion laughs" shape, which is what YAML's alias facility costs unless something counts. There is no published value to follow, so this takes the safe side — enough for any document anyone meant to write, far short of exhausting memory. Flavor::new(budget=...) raises it, and aliases=false refuses an alias outright.

    core

    let core : Schema

    The core schema of YAML 1.2.2 §10.2: what this package reads.

    core_rules

    let core_rules : Rules

    YAML 1.2's core schema: nothing but true/false, null, and the decimal, 0o and 0x numbers of §10.2.

    declared_version

    fn declared_version(src : StringView) -> (Int, Int)?

    The version a stream declares in its first %YAML directive, if it declares one.

    A reader may pick its rules from the document itself rather than from the caller, which is what [any] does. Only the first directive counts: §6.8.1 allows one per document, and the one that decides how the rest is read has to come before the rest.

    depth

    let depth : Int

    How deep a document may nest.

    500 is deep enough for any configuration file ever written and shallow enough that a reader which walks the tree cannot be driven off the stack by one that was not. A document that nests deeper than this is not a configuration file, and is refused rather than read.

    dump

    fn dump(value : Json, style? : Style, rules? : Rules, indent? : Int, sort? : Bool) -> Bytes

    The same, as UTF-8 bytes — the s on [dumps] is the one Python puts on the form that answers a string.

    dump_all

    fn dump_all(values : ArrayView[Json], style? : Style, rules? : Rules, indent? : Int, sort? : Bool) -> Bytes

    The same, as UTF-8 bytes.

    dumps

    fn dumps(value : Json, style? : Style, rules? : Rules, indent? : Int, sort? : Bool) -> String

    Write a Json tree as a YAML document.

    Block style by default, which is what a configuration file is written in and what a person reads. A string is quoted only when plain would read back as something else — a number, a boolean, a null, or not at all — so the output looks like a file someone wrote rather than one a program emitted.

    This is the reason to have a writer at all: a generator that builds YAML by joining strings gets the escaping wrong the first time a value contains a colon, and gets the indentation wrong the first time a value is a list.

    style says how; indent and sort override the two knobs that change from call to call. A flat parameter wins over the record — see "Configuration" in the README.

    dumps_all

    fn dumps_all(values : ArrayView[Json], style? : Style, rules? : Rules, indent? : Int, sort? : Bool) -> String

    A stream of documents, each opened by its own ---.

    The separator goes before each document rather than between them, which is the one convention that stays correct under concatenation: two streams joined end to end still read as their documents, and appending one is appending lines. A reader of either convention reads this one — a marker before the first document is not an error anywhere — so nothing is given up by choosing it.

    dumps_of

    fn[T : ToJson] dumps_of(value : T, style? : Style, indent? : Int, sort? : Bool) -> String

    A value of type T, written as a YAML document.

    The other direction of [loads_as], through the same ToJson trait, so a type that writes itself as JSON writes itself as YAML without knowing it.

    dumps_of_all

    fn[T : ToJson] dumps_of_all(values : ArrayView[T], style? : Style, indent? : Int, sort? : Bool) -> String

    A stream of values, each written as its own document.

    emit

    fn emit(node : Node, indent? : Int, from? : Int) -> String

    Emit a document as YAML, keeping the styles and comments it was read with.

    indent is how far a nested block is indented, and from is how far the root already is — a document embedded in another one needs the second.

    A document that was written under an explicit --- gets one back, on a line of its own; the comments above it stay above it.

    events_all

    fn events_all(nodes : Array[Node]) -> String

    Every document's events, between the +STR and -STR that open and close a stream — one line per event, which is what makes two streams comparable line by line.

    flow

    let flow : Style

    The same document, all in flow style — the form JSON shares.

    indent

    let indent : Int

    How much a nested level is indented.

    Two spaces: what almost every configuration file in the wild uses, and what a reader's eye is trained on.

    legacy_rules

    let legacy_rules : Rules

    YAML 1.1's type resolution, §10.3 of the 1.1 specification: the booleans, the octal and binary integers, the underscored numbers, and the base-60 form.

    load

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

    The one document in src, decoded as UTF-8.

    bom skips a leading byte-order mark, which YAML 1.2.2 §5.2 allows at the start of a stream and which an editor on Windows will happily write.

    load_all

    fn load_all(src : BytesView, flavor? : Flavor, schema? : Schema, bom? : Bool) -> Array[Json] raise Refused

    Every document in src, decoded as UTF-8.

    load_as

    fn[T :
    FromJson
    ] load_as(src : BytesView, flavor? : Flavor, schema? : Schema, bom? : Bool) -> T raise Malformed

    The one document in src, decoded as UTF-8, as a value of type T.

    loads

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

    The one document in src.

    Raises [Refused::Trailing] if a second document follows, which is what tells a caller expecting one file's worth of configuration that it got a stream.

    schema says which YAML version reads it; the default is [core]. A document that opens with %YAML 1.1 is not read as 1.2 by default — pick the version, or let the any package pick it for you.

    loads_all

    fn loads_all(src : StringView, flavor? : Flavor, schema? : Schema) -> Array[Json] raise Refused

    Every document in src, in order.

    A stream with no documents at all is an empty array, not a null: nothing is not the same as one document containing nothing.

    loads_as

    fn[T :
    FromJson
    ] loads_as(src : StringView, flavor? : Flavor, schema? : Schema) -> T raise Malformed

    The one document in src, as a value of type T.

    let config : Config = @moonyaml.loads_as(src)

    T is anything that implements core's FromJson, which is the same trait a JSON document is read through: a type written for moonjson needs no second implementation to be read from YAML. That is the point of reading into the same Json tree — the shape of a value is one question, and which text it arrived in is another.

    The default schema is 1.2's core. Read a 1.1 file with @moonyaml/v11's schema, or let @moonyaml/any pick it from the document.

    parse

    fn parse(src : StringView, flavor? : Flavor, schema? : Schema) -> Node raise Refused

    One document, as a tree.

    parse_all

    fn parse_all(src : StringView, flavor? : Flavor, schema? : Schema) -> Array[Node] raise Refused

    Every document in the stream, as trees.

    A stream with nothing in it holds no document, which is what loads_all says too; parse is where a single document is owed, and it gives the null one.

    round_trip

    fn[T :
    FromJson
    + ToJson] round_trip(src : StringView, adjust? : (T) -> Unit, flavor? : Flavor, schema? : Schema) -> String raise Malformed

    Read a document as T, let adjust change it, and write it back.

    The three steps a configuration loader actually takes: read what the user wrote, fill in what they left out, write the whole of it back. adjust is where the defaults go, and it is also what says which type this is — a call with no adjust cannot tell.

    strict

    let strict : Flavor

    The preset: refuse what is ambiguous, take the last of a repeated key, expand aliases within the budget.

    style

    let style : Style

    The preset: two-space block style, folded at 80 columns, quoting only what has to be quoted, anchors for what repeats.

    Block, not Auto: what this writes is a configuration file, and a configuration file is read one entry to a line. Auto is a knob for output whose reader is a machine.

    width

    let width : Int

    The column a writer folds at.

    80: the width yamllint defaults to, which is itself the width of a terminal and of the Python PEP 8 convention the tooling around YAML grew up beside.

    Powered by MoonBit

    Site sourceReport issueModulesSkillsBuild queueStatistics

    © 2026 mooncakes.io