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)
endA 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 absentKeeping 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())
endSafe 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 sessionLenient 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.