iyi

Standard library, 0.18.0

std/yaml

YAML, read and written in iyi alone: a parser and an emitter for the YAML 1.2 core schema over the bytes of a String, with no library under them.

import std/yaml to use it. Source: src/std/yaml.iyi at v0.18.0.

What it reads:

* Block mappings and block sequences, nested by indentation; a sequence
  may sit at the same indentation as the key it belongs to; `- key: v`
  opens a mapping inside a sequence entry.
* Flow collections `[a, b]` and `{a: b}`, nested, across lines, with a
  trailing comma allowed and `[k: v]` as a one-pair mapping.
* Plain scalars, folded across lines; single-quoted scalars with `''`;
  double-quoted scalars with every escape the format names (`\n`, `\t`,
  `\"`, `\\`, `\/`, `\0`, `\a`, `\b`, `\e`, `\f`, `\r`, `\v`, `\ `,
  `\N`, `\_`, `\L`, `\P`, `\xXX`, `\uXXXX`, `\UXXXXXXXX`, an escaped line
  break), both quoted styles folded across lines.
* Block scalars `|` and `>` with `-`, `+` and an explicit indentation
  digit, chomped as the header says.
* Anchors `&a`, aliases `*a`, merge keys `<<: *a` and `<<: [*a, *b]`.
  A key the mapping spells itself wins over a merged one, wherever it is
  spelled; among merged mappings the first wins.
* The core-schema tags `!!str`, `!!int`, `!!float`, `!!bool`, `!!null`,
  `!!seq` and `!!map`, on a value or a key; any other tag is refused.
* Streams: `%YAML` and `%TAG` directives are skipped, `---` opens a
  document and `...` ends one. `parse_all` answers every document;
  `parse` answers the first (nil for an empty stream) after checking the
  whole stream.
* Plain scalars resolve as the 1.2 core schema says: `null`, `Null`,
  `NULL`, `~` and the empty scalar are nil; `true`/`false` in the three
  spellings are booleans; decimal, `0o` octal and `0x` hexadecimal are
  `Int64`; `1.5`, `.5`, `7.`, `1e3`, `1.0e+20`, `.inf`, `-.inf`, `.nan`
  are `Float64`; a decimal integer past the `Int64` range is read as a
  `Float64`; everything else is a `String`. So `yes`, `no`, `on`,
  `off`, `1_000`, `1.2.3`, `22:30` and `2001-12-14` are strings here,
  and `0777` is the decimal 777, where a 1.1 reader would make some of
  them booleans, octal numbers, or a timestamp. Keys resolve the same
  way: `1: one` has the `Int64` key 1, `true: x` the `Bool` key true.

What it refuses, each with a sentence that says where:

a UTF-16 stream (named by its byte order mark), a tab in indentation,
a control character (C0, DEL or C1, but tab and U+0085) or the
noncharacter U+FFFE or U+FFFF, anywhere, a quoted string that does
not close, a flow collection that does not close, an unknown escape or
an escape that names a surrogate, a mapping entry or sequence entry at
an indentation that belongs to nothing, text after a value on its
line, a value whose text is not a node, a duplicate key, a second `<<`
among them (the second spelling of a key is a mistake, not an
override; `<<: [*a, *b]` merges two), an alias to an anchor that is
not defined or is still being built (a cycle), aliases that together
expand to more than ALIAS_NODE_LIMIT nodes (the "billion laughs"
stream), a merge whose value is not a mapping or a sequence of
mappings, an explicit `?` key, an anchor or an alias on a block
mapping's key (a flow mapping's key may carry either), a collection
as a key, a second tag or a second anchor on one node, a tag it
does not know (the verbatim `!<tag:yaml.org,2002:str>` spellings of
the core tags are known), a directive not followed by `---`, and
nesting past DEPTH_LIMIT levels.

YAML.parse? answers nil for any of these; YAML.parse panics with the sentence. YAML.dump writes a value back as block YAML that this parser reads to an equal value: numbers, booleans and nil stay what they are as values and as keys, a string that would read as something else is quoted, and a character a reader would not take raw (a control, a byte order mark, a 1.1 line break) is escaped. "Something else" includes what a YAML 1.1 reader makes of it, since PyYAML is what reads most of these files on the other end: yes, off, = and anything starting with a digit are quoted too. A collection cannot be written as a key.

derive yaml_serializable makes a type read and write itself as a mapping, a key per field, as std/json's derive serializable does for JSON; YAML.to_yaml, YAML.from_yaml, YAML.to_any and YAML.read_value are what it writes with.

Any holds a document: raw is the union, as_x/as_x? are the pairs, []/[]?/dig/dig? walk it, == is structural and hash agrees with it.

The type tests on Any#raw are ordered with the reference types first. A chain that tests Nil and Bool before Array/Hash over this union sends an array down the hash arm today (a compiler defect being fixed); every dispatch here goes through Any#kind, which is written in the order that works, and nothing else in the file tests the union.

yaml_serializablemacro

derive yaml_serializable, in a struct or class whose fields are written with their types: the type reads and writes itself as a YAML mapping, a key per field, named as the field is, in the order the fields are written.

  pub struct Coord
    getter x : Float64
    getter name : String
    derive yaml_serializable
  end
  Coord.from_yaml("x: 1.5\nname: a\n").x   # => 1.5
  coord.to_yaml                            # => "x: 1.5\nname: a\n"

It is std/json's derive serializable over YAML, field for field: a value is read and written as YAML.read_value and YAML.to_any read and write its type (the built-in scalars, an Array or Hash of them, an Any, another serializable type, each of them or nil), and to_yaml writes through YAML.dump, so a string that would read as something else is quoted. A key the type has no field for is passed over; a field with no key keeps its default when it is written with one, as does a field with a default whose key holds null where its type does not admit nil; a field with neither is refused by name unless its type admits nil, which it then holds. Anchors, aliases and << merges are the parser's, so a mapping merged into another fills its fields.

The macro is named for its format because a derive names a macro bare: with std/json's serializable imported as well, a second serializable was refused as "'serializable' is ambiguous here", and derive takes no qualified name. A type that is both writes derive serializable, yaml_serializable; what the two generate shares no method (to_json, from_json and initialize(pull) against to_yaml, from_yaml, to_yaml_any and initialize(yaml)).

std/yamliyi doc src/std/yaml.iyi
pub macro yaml_serializable(declaration)

Anystruct

The value a document holds.

std/yamliyi doc src/std/yaml.iyi
pub struct Any
  alias Type = Nil | Bool | Int64 | Float64 | String | Array(Any) | Hash(Any, Any)
  def !=(other : Any) : Bool
  # Structural: same kind, same value; a mapping equals a mapping with the
  # same pairs in any order. `1` and `1.0` are different values.
  def ==(other : Any) : Bool
  def [](key : Any) : Any
  def [](key : String) : Any
  # An Int32 indexes a sequence and reaches an integer key of a mapping:
  # `1: one` has the `Int64` key 1, and `doc[1]` panicked "expected a
  # sequence, not a mapping" while `[]?` and `dig?` answered nil.
  def [](index : Int32) : Any
  def []?(key : Any) : Any | ::Nil
  def []?(key : String) : Any | ::Nil
  def []?(index : Int32) : Any | ::Nil
  def as_a : Array(Any)
  def as_a? : Array(Any) | ::Nil
  def as_bool : Bool
  def as_bool? : Bool | ::Nil
  # A float, or an integer widened to one.
  def as_f : Float64
  def as_f? : Float64 | ::Nil
  def as_h : Hash(Any, Any)
  def as_h? : Hash(Any, Any) | ::Nil
  def as_i : Int64
  def as_i? : Int64 | ::Nil
  def as_nil : Nil
  def as_nil? : Nil
  def as_s : String
  def as_s? : String | ::Nil
  def dig(key : String | Int32) : Any
  def dig(key : String | Int32, *rest : String | Int32) : Any
  def dig?(key : String | Int32) : Any | ::Nil
  def dig?(key : String | Int32, *rest : String | Int32) : Any | ::Nil
  # Agrees with `==`: the same kind and value hash the same, a mapping's
  # hash does not depend on its order, and `0.0`/`-0.0` share one. A
  # sequence mixes its items as an `Array` does, a mapping sums one mixed
  # tuple hash per pair, and an integer hashes as an `Int64`. `31 * h +
  # item`, a sum of `31 * key + value` and the integer's own low bits
  # gave a 150 by 150 grid of `[i, j]` 4,769 hashes and of `{x: i, y: j}`
  # 299, `{a: 1, b: 2}` the hash of `{a: 2, b: 1}` and -1 that of 0, and
  # 40,000 such mappings took 987 ms to insert as keys; they take 19 ms.
  def hash : Int32
  def initialize(raw : Type)
  def initialize(value : Int32)
  def inspect : String
  # Which member of the union `raw` is, as one of the KIND_ constants.
  # Reference types are tested first on purpose (see the header).
  def kind : Int32
  # The name of the kind, for the sentences below.
  def kind_name : String
  def null? : Bool
  def raw : Type
  # Elements of a sequence, pairs of a mapping, characters of a string.
  def size : Int32
  # A string as itself, a number or boolean as written, nil as nothing,
  # a collection the way the prelude prints an array or a hash.
  def to_s : String
end

Parserclass

Reads one stream. Every failure is recorded once in @error and every loop returns as soon as one is recorded; nothing is raised inside.

std/yamliyi doc src/std/yaml.iyi
pub class Parser
  def error : String | ::Nil
  def initialize(text : String)
  # Every document in the stream, or nil with `error` set.
  def parse_all : Array(Any) | ::Nil
end

ToYAMLtrait

A type that can write itself as an Any. YAML.to_yaml and YAML.to_any accept any value whose type implements it, as a derive yaml_serializable type does.

std/yamliyi doc src/std/yaml.iyi
pub trait ToYAML
  abstract def to_yaml_any : Any
end

YAMLstruct

std/yamliyi doc src/std/yaml.iyi
pub struct YAML
  # Block YAML for `value`, ending in a line break.
  def self.dump(value : Any) : String
  # The document `text` holds, read into `type`: `Nil`, `Bool`, `Int32`,
  # `Int64`, `UInt8`, `UInt64`, `Float64`, `String`, `Any`, a type with
  # `derive yaml_serializable`, one of those or `nil`, or an `Array` or a
  # `Hash` of those. Text that is not YAML panics as `parse` does; a value
  # of another kind, or an integer outside the type, is refused.
  def self.from_yaml(text : String, type : T.class) : T forall T
  def initialize
  def self.parse(text : String) : Any
  # The first document of `text`, or nil when the stream is not YAML this
  # module reads (the whole stream is checked, not just the first
  # document). An empty stream is a null document.
  def self.parse?(text : String) : Any | ::Nil
  def self.parse_all(text : String) : Array(Any)
  def self.parse_all?(text : String) : Array(Any) | ::Nil
  def self.read_value(value : Any, type : Nil.class) : Nil
  def self.read_value(value : Any, type : Bool.class) : Bool
  def self.read_value(value : Any, type : Int32.class) : Int32
  def self.read_value(value : Any, type : Int64.class) : Int64
  def self.read_value(value : Any, type : UInt8.class) : UInt8
  def self.read_value(value : Any, type : UInt64.class) : UInt64
  # A float, or an integer widened to one: `y: 2` is the Float64 2.0.
  def self.read_value(value : Any, type : Float64.class) : Float64
  def self.read_value(value : Any, type : String.class) : String
  def self.read_value(value : Any, type : Any.class) : Any
  def self.read_value(value : Any, type : Array(T).class) : Array(T) forall T
  def self.read_value(value : Any, type : Hash(K, V).class) : Hash(K, V) forall K, V
  # A type that reads itself, the `initialize(yaml)` a `derive
  # yaml_serializable` wrote; a type that admits nil reads null as the nil
  # and anything else as the other type, as std/json's `read_value` does.
  def self.read_value(value : Any, type : T.class) : T forall T
  # The `Any` a value is written as: the value or `nil`, the way
  # `JSON.to_json` takes it. A nil is taken out here, before the rest
  # dispatches, for the reason std/json's `write_or_null` gives: handed to
  # `write_value` whole, `Int32 | Nil` matches the `ToYAML` overload as
  # one type and does not build.
  def self.to_any(val : T) : Any forall T
  # Block YAML for any value `to_any` knows, written as `dump` writes it.
  def self.to_yaml(obj : T) : String forall T
  def self.write_value(val : Nil) : Any
  def self.write_value(val : Bool) : Any
  def self.write_value(val : Int32) : Any
  def self.write_value(val : Int64) : Any
  def self.write_value(val : UInt8) : Any
  # A YAML integer is read as an `Int64`, and one past it as a float, so a
  # `UInt64` above 9223372036854775807 would read back as a different
  # number: it is refused rather than written.
  def self.write_value(val : UInt64) : Any
  def self.write_value(val : Float64) : Any
  def self.write_value(val : String) : Any
  def self.write_value(val : Any) : Any
  def self.write_value(val : Array(T)) : Any forall T
  # A key of any type `to_any` writes as a scalar: YAML keys are typed,
  # so `Hash(Int32, String)` is written with integer keys and reads back
  # as itself. A collection key is refused by `dump`.
  def self.write_value(val : Hash(K, V)) : Any forall K, V
  def self.write_value(val : T) : Any forall T : ToYAML
end