Skip to content

Releases: contact-red/yaml

Release list

Initial Release

Choose a tag to compare

@redvers redvers released this 17 Aug 01:02

Initial Release

A YAML reader for Pony.

Parsing never decides what a scalar means. 1.20 is the text 1.20, NO is the text NO, 0755 is the text 0755. Meaning is assigned when you ask for a type, so a country code does not become false and a version string does not become a float.

Reading a document

YamlLoad parses a source and binds it onto one of your own types. You get your value, or every problem found while producing it.

use "yaml"

actor Main
  new create(env: Env) =>
    let source = "host: example.com\nport: 8443\n"

    match YamlLoad[(String val, U16)](source,
      {(c) => (c("host").text_or_else("localhost"),
               c("port").int_or_else[U16](8080)) })
    | (let host: String val, let port: U16) =>
      env.out.print(host + ":" + port.string())
    | let f: YamlFailure =>
      env.err.print(f.string())
      env.exitcode(1)
    end

A decoder is a function over a view. Its problems are collected as it runs, so a configuration with four mistakes in it reports four, not the first one.

Absence has one rule. An extractor you gave a default to is silent when the key is absent, because the default is your answer. Everything else — text(), sequence, entries, a nested decode records one problem unless you mark the position optional().

c("tls")("enabled").bool_or_else(false)              // silent if tls is absent
c("tls").optional()("enabled").bool_or_else(false)   // silent, and says so
c("endpoints").sequence[Endpoint](EndpointOf)        // reports if absent

Keeping the good elements

attempt runs a decoder in a session of its own, so one malformed element does not fail the rest.

c("endpoints").sequence[(Endpoint | YamlProblems)](
  {(e) => e.attempt[Endpoint](EndpointOf) })

A failed element is a different type from a real one, so it cannot be mistaken for a listener on 0.0.0.0 with TLS off.

Mappings whose keys you do not know

c("datasources").entries[Datasource](
  {(name, v) => Datasource(name, v("driver").text_or_else("")) })

The key reaches your lambda as data and never becomes part of a path, so a mapping keyed by connection strings cannot put a password into an error message — the segment renders as <key at line 4, column 3>.

Two more ways in

YamlParser.parse returns a YamlNode you walk yourself, for inspecting a manifest rather than loading settings. YamlCursor is the event stream with no tree built, for scanning something large for one field.

match YamlParser.parse(source)
| let m: YamlMapping =>
  for (key, value) in m.pairs() do
    env.out.print(key.text())
  end
| let e: YamlParseError => env.err.print(e.string())
end

Safe to log

No node type has a string(), and no error or problem carries document bytes except a path segment you supplied yourself. env.err.print(f.string()) is safe on a document full of credentials.

To keep an extracted value out of your own output, wrap it with sensitive. A node cannot be wrapped — Sensitive[A] requires A to be Stringable and no node type is so extract the value first.

let token = Sensitive[String](c("auth")("token").text())
env.out.print("token: " + token.string())   // token: [REDACTED]

Neither mechanism defends against someone who can already read the process. Both stop a secret reaching a log.

Strict and lenient

A quoted scalar is a string by default, per the YAML 1.2 core schema: asking for an integer and finding "8080" is a problem, not an 8080. Quoting is how a templating layer asserts that a value stays text.

When your documents quote everything they interpolate, say so:

c("port").resolving(YamlLenient).int_or_else[U16](8080)      // one key
YamlLoad[Config](src, decode where resolution = YamlLenient) // the session

Lenient also accepts the YAML 1.1 boolean words, so country: NO becomes false if you ask for a Bool.

Two refusals apply in both modes, and neither is what the core schema says. An integer with a leading zero and more than one digit is refused, because a file mode written the way every Unix tool writes it should not become a different, plausible number; ask for it with int_radix_or_else[U32](8, default). .nan is refused, because every comparison against it is false and a scalar naming it would switch off a limit with no error anywhere. .inf is accepted.

What is supported

Block and flow collections, all five scalar styles, multi-document streams, and the YAML 1.2 core schema.

Anchors, aliases, merge keys, custom tags, ? explicit keys and collections used as mapping keys are not. A document using one is refused with an error naming the feature, so you learn which part of YAML the document needed rather than reading a syntax error.

A document is also refused for a duplicate key, including one that is a duplicate only after core-schema resolution: {1: allow, 0x1: deny} is one key to every other implementation, which reads it last-wins as deny.

Bounds

max_depth (1024) and max_nodes (1,000,000) are parameters on every entry point. A node costs roughly 92 to 460 bytes depending on shape, so the default bounds a tree at about 460 MB in the worst case from around 8 MB of source. Lower both for untrusted input; the size of the source alone is not a usable bound, which is why max_nodes exists.

max_depth bounds your own recursion over a parsed tree, not the parser's. The parser uses an explicit stack and never recurses on input depth, so a deeply nested document produces an error rather than exhausting the stack.

Conformance

The official yaml-test-suite runs as part of the test suite. All 402 cases behave as the corpus says: 211 match its event stream exactly, 94 are refused as invalid, 89 are refused as unsupported with the feature named, and 8 are deliberate differences recorded with their reasons refusing an empty input, and refusing %YAML 1.1 rather than reading it as 1.2.

API Documentation

Generate with make docs.