moonyamlmoonbitstack

examples · three programs

Nothing here is a mock-up.

Three programs in src/examples/, each runnable with one command, and the output each of them prints — captured from a real run, not written by hand. If a number in this page were wrong, the program next to it would disagree.

01 · the tour

Everything it reads, in one file.

A configuration file of the shape one is actually written in: block collections, a literal scalar that keeps its line breaks, an anchor reused further down, a flow sequence. Then the core schema deciding what an unquoted scalar is, a stream of two documents, and four refusals with the position each one is at.

moon run src/examples/tour

what it prints
read as JSON:
  {"server":{"host":"0.0.0.0","port":12000,"motd":"welcome\nto the party\n"},"limits":{"files":1000,"bytes":1048576},"upload":{"files":1000,"bytes":1048576},"tags":["http","native"]}
written back:
  server:
    host: 0.0.0.0
    port: 12000
    motd: |
      welcome
      to the party
  limits: &a0
    files: 1000
    bytes: 1048576
  upload: *a0
  tags:
    - http
    - native
resolution:
  [1,1.5,31,true,null,"yes","1"]
stream of 2 documents:
  {"first":1}
  {"second":2}
  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: !!nope 1 => {"a":1}
  a: &a [x, x] ⏎ b: &b [*a, *a] ⏎ c: &c [*b, *b] ⏎ d: [*c, *c] ⏎  => 2:8: expands to 9 nodes, over 8
src/examples/tour/main.mbt
///|
/// Everything moonyaml reads, in one file.
///
///     moon run src/examples/tour
#coverage.skip
fn main {
  run() catch {
    e => println("refused: \{e}")
  }
}

///|
fn run() -> Unit raise {
  // A configuration file, of the shape one is actually written in: block
  // collections, a literal scalar that keeps its line breaks, and an anchor
  // reused further down.
  let config =
    #|server:
    #|  host: 0.0.0.0
    #|  port: 12000
    #|  motd: |
    #|    welcome
    #|    to the party
    #|limits: &shared {files: 1000, bytes: 1048576}
    #|upload: *shared
    #|tags: [http, native]
    #|
  let tree = @moonyaml.loads(config)
  println("read as JSON:")
  println("  \{tree.stringify()}")
  println("written back:")
  for line in @moonyaml.dumps(tree).split("\n") {
    if line.length() > 0 {
      println("  \{line}")
    }
  }

  // The core schema decides what an unquoted scalar is; quoting it makes it a
  // string whatever it looks like.
  let scalars =
    #|[1, 1.5, 0x1f, true, ~, yes, "1"]
    #|
  println("resolution:")
  println("  \{@moonyaml.loads(scalars).stringify()}")

  // A stream is several documents, and each is read in its own right.
  let stream =
    #|---
    #|first: 1
    #|---
    #|second: 2
    #|...
    #|
  let docs = @moonyaml.loads_all(stream)
  println("stream of \{docs.length()} documents:")
  for d in docs {
    println("  \{d.stringify()}")
  }

  // Ambiguity is refused with a position rather than guessed at.
  show_refusal("a: [1, 2")
  show_refusal("a: *nope")
  show_refusal("a: 1\na: 2", flavor=@moonyaml.Flavor::new(duplicates=Reject))

  // A tag from a vocabulary this reader does not have is not a fault: §10.4 says
  // the node resolves as if it carried no tag, and refusing it would refuse the
  // `!!python/object` in a file somebody else wrote.
  println("  a: !!nope 1 => \{@moonyaml.loads("a: !!nope 1").stringify()}")

  // And an alias that expands past its budget is refused too, which is what
  // stops a dozen lines from becoming billions of nodes.
  show_refusal(
    "a: &a [x, x]\nb: &b [*a, *a]\nc: &c [*b, *b]\nd: [*c, *c]\n",
    flavor=@moonyaml.Flavor::new(budget=8),
  )
}

///|
/// Read a document that will not load, and print why.
fn show_refusal(
  src : String,
  flavor~ : @moonyaml.Flavor = @moonyaml.strict,
) -> Unit {
  let one_line = src.replace_all(old="\n", new=" ⏎ ")
  let said = @moonyaml.loads(src, flavor~).stringify() catch {
    e => e.to_string()
  }
  println("  \{one_line} => \{said}")
}

Two things to notice. dumps invented the anchor name &a0, because a value has no anchor names — the Json tree only knows that upload and limits are the same object. And the last line is the "billion laughs" cut short: four lines of aliases that would expand to sixteen nodes, refused the moment the count passes a budget of eight, with the count and the limit in the message.

02 · the versions

One document, read five ways.

yes is a boolean in 1.1 and a string in 1.2. 014 is octal twelve in 1.1 and decimal fourteen in 1.2. 12:30 is a sexagesimal only in 1.1. The same file, five readings, and not one of them is a bug:

moon run src/examples/versions

what it prints
the document:
  enabled: yes
  mask: 014
  starts: 12:30
  when: 2001-12-15 02:59:43

read as:
  1.2.2 (core schema): {"enabled":"yes","mask":14,"starts":"12:30","when":"2001-12-15 02:59:43"}
  1.1 (legacy types, merge key): {"enabled":true,"mask":12,"starts":750,"when":"2001-12-15 02:59:43"}
  1.0 (1.0 types): {"enabled":true,"mask":12,"starts":750,"when":"2001-12-15 02:59:43"}

and as StrictYAML, where every scalar is a string:
  strict: {"enabled":"yes","mask":"014","starts":"12:30","when":"2001-12-15 02:59:43"}
  strict, on flow style: refused — 1:4: flow style is not allowed here
  strict, on an anchor: refused — 1:4: an anchor is not allowed here

and by the version the document asks for:
  any, no directive: {"enabled":"yes","mask":14,"starts":"12:30","when":"2001-12-15 02:59:43"}
  any, %YAML 1.1: {"enabled":true,"mask":12}
  any, %YAML 1.2: {"enabled":"yes","mask":14}
src/examples/versions/main.mbt
// The same document, read by every version this library implements.
//
//     moon run src/examples/versions

///|
#coverage.skip
fn main {
  // A document written for no version in particular: a configuration file with a
  // word, an octal and a time in it. Which of those are what they look like is
  // the whole of the difference between the versions.
  let src =
    #|enabled: yes
    #|mask: 014
    #|starts: 12:30
    #|when: 2001-12-15 02:59:43
    #|

  println("the document:")
  for line in src.split("\n") {
    if line.length() > 0 {
      println("  " + line.to_owned())
    }
  }
  println("")
  println("read as:")
  each("1.2.2 (core schema)", src, text => @moonyaml.loads(text))
  each("1.1 (legacy types, merge key)", src, text => @yaml11.loads(text))
  each("1.0 (1.0 types)", src, text => @yaml10.loads(text))

  // StrictYAML is not a version of YAML with different scalars: it is a language
  // that keeps the shape of YAML and drops everything that is ambiguous. Every
  // scalar it reads is a string, and flow style, anchors, aliases, tags and a key
  // written twice are refused.
  println("")
  println("and as StrictYAML, where every scalar is a string:")
  each("strict", src, text => @strict.loads(text))
  each("strict, on flow style", "a: [1, 2]", text => @strict.loads(text))
  each("strict, on an anchor", "a: &x 1", text => @strict.loads(text))

  // A stream's first document may say which version the rest are written for,
  // which is what `any` reads to pick a schema; without a directive it is 1.2.2.
  println("")
  println("and by the version the document asks for:")
  each("any, no directive", src, text => @any.loads(text))
  each("any, %YAML 1.1", "%YAML 1.1\n---\nenabled: yes\nmask: 014\n", text => {
    @any.loads(text)
  })
  each("any, %YAML 1.2", "%YAML 1.2\n---\nenabled: yes\nmask: 014\n", text => {
    @any.loads(text)
  })
}

///|
/// Read `src` one way and print what came back, or the refusal.
fn each(
  name : String,
  src : String,
  read : (StringView) -> Json raise @moonyaml.Refused,
) -> Unit {
  let said = read(src[:]).stringify() catch {
    e => "refused — " + e.to_string()
  }
  println("  \{name}: \{said}")
}

The last three lines are any, which is the only reader here that looks at the document to decide: %YAML 1.1 at the top makes yes a boolean, %YAML 1.2 makes it a string, and no directive at all means 1.2.2.

03 · the document

Read it, write it, and get the file back.

parse gives a tree that keeps what the file said — the comment above the document, the comment beside the image line, the anchor, the alias standing in for its value, the flow style of ports, the literal block scalar. emit writes it back and the program compares the two strings:

moon run src/examples/document

what it prints
the document:
  # The deployment this file describes.
  name: web
  defaults: &defaults
    replicas: 3
    image: nginx:1.27
  front:
    <<: *defaults
    image: nginx:1.27-alpine # the only difference
  ports: [80, 443]
  motd: |
    one
    two

written back: is it the same document?
  yes, byte for byte

and what it means:
  {"name":"web","defaults":{"replicas":3,"image":"nginx:1.27"},"front":{"<<":{"replicas":3,"image":"nginx:1.27"},"image":"nginx:1.27-alpine"},"ports":[80,443],"motd":"one\ntwo\n"}

a document built from a value, with an indent of four:
  a: 1
  b:
      - x
      - y: 2

written in flow style, ten deep:
  [1, 2, 3] 
src/examples/document/main.mbt
// A document read as a document: what it says, not only what it means.
//
//     moon run src/examples/document

///|
#coverage.skip
fn main raise {
  let src =
    #|# The deployment this file describes.
    #|name: web
    #|defaults: &defaults
    #|  replicas: 3
    #|  image: nginx:1.27
    #|front:
    #|  <<: *defaults
    #|  image: nginx:1.27-alpine # the only difference
    #|ports: [80, 443]
    #|motd: |
    #|  one
    #|  two
    #|

  println("the document:")
  for line in src.split("\n") {
    if line.length() > 0 {
      println("  " + line.to_owned())
    }
  }

  // `parse` keeps the comments, the flow style of `ports`, the anchor on
  // `defaults` and the alias in place of its value — everything `loads` folds
  // into a `Json` tree on the way to being a value.
  let doc = @moonyaml.parse(src)

  println("")
  println("written back: is it the same document?")
  let written = @moonyaml.emit(doc)
  println(if written == src { "  yes, byte for byte" } else { "  no:" })
  if written != src {
    for line in written.split("\n") {
      println("  " + line.to_owned())
    }
  }

  // What it means, for a caller who wants the value: the merge key folded in, the
  // alias resolved, the tags gone.
  println("")
  println("and what it means:")
  println("  " + doc.to_json().stringify())

  // A tree built from a value is a document too, and it writes as one.
  println("")
  println("a document built from a value, with an indent of four:")
  let value = @moonyaml.loads(
    (
      #|a: 1
      #|b:
      #|  - x
      #|  - y: 2
      #|
    ),
  )
  let built = @moonyaml.Node::of_json(value)
  for line in @moonyaml.emit(built, indent=4).split("\n") {
    if line.length() > 0 {
      println("  " + line.to_owned())
    }
  }

  // The writer's other knob: how far a line may run before a collection is written
  // as a block instead.
  println("")
  println("written in flow style, ten deep:")
  let list = @moonyaml.loads(
    (
      #|- 1
      #|- 2
      #|- 3
      #|
    ),
  )
  println(
    "  " +
    @moonyaml.dumps(list, style=@moonyaml.flow).replace_all(old="\n", new=" "),
  )
}

"yes, byte for byte" is printed only when emit(parse(src)) == src. Below it, the same document through to_json — which is where the alias stops being an alias and the merge key is folded in — then a tree built from a value at indent=4, then the writer in flow style.

04 · run them

Three commands.

# everything it reads, in one file
moon run src/examples/tour

# one document, read five ways
moon run src/examples/versions

# the document model, and a round trip
moon run src/examples/document
guide

The guide

Reading, writing, the five versions, every knob and where its default comes from.

reference

The API

Every public type, value and function, with the signature the compiler produces.

source

src/examples

The three programs above, in the repository, each runnable on its own.