aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakob Odersky <jakob@odersky.com>2019-10-22 00:55:50 -0400
committerJakob Odersky <jakob@odersky.com>2019-11-08 22:34:57 -0500
commit99dfd5a3ececf39ae3fd30cbf9581c2fb5da2ba5 (patch)
tree59dd8a2f5283d436b300478b7197bb8ec9f5164e
parent70141fc60ec3341057627e9a8f5b83a22c74f0ea (diff)
downloadyamlesque-0.2.0.tar.gz
yamlesque-0.2.0.tar.bz2
yamlesque-0.2.0.zip
Major refactor for version 0.2.00.2.0
-rw-r--r--.gitignore3
-rw-r--r--.mill-version1
-rw-r--r--.travis.yml21
-rw-r--r--README.md117
-rw-r--r--build.sbt50
-rw-r--r--build.sc61
-rwxr-xr-xmksite26
-rw-r--r--project/build.properties1
-rw-r--r--project/plugins.sbt9
-rwxr-xr-xpublish19
-rw-r--r--publish.sbt33
-rw-r--r--site/index.html67
-rw-r--r--site/src/Main.scala40
-rw-r--r--yamlesque-spray-json/src/main/scala/formats.scala44
-rw-r--r--yamlesque-spray-json/src/test/scala/FormatTests.scala53
-rw-r--r--yamlesque/src/Parser.scala406
-rw-r--r--yamlesque/src/Writer.scala74
-rw-r--r--yamlesque/src/YamlNodes.scala81
-rw-r--r--yamlesque/src/main/scala/YamlParser.scala258
-rw-r--r--yamlesque/src/main/scala/YamlPrinter.scala48
-rw-r--r--yamlesque/src/main/scala/formats.scala8
-rw-r--r--yamlesque/src/main/scala/package.scala26
-rw-r--r--yamlesque/src/main/scala/yamlValues.scala23
-rw-r--r--yamlesque/src/package.scala30
-rw-r--r--yamlesque/src/test/scala/ParserTests.scala221
-rw-r--r--yamlesque/test/src/BasicTest.scala196
-rw-r--r--yamlesque/test/src/NegTest.scala96
-rw-r--r--yamlesque/test/src/StreamTest.scala54
-rw-r--r--yamlesque/test/src/StringTest.scala33
-rw-r--r--yamlesque/test/src/VerbatimTest.scala114
30 files changed, 1389 insertions, 824 deletions
diff --git a/.gitignore b/.gitignore
index 2f7896d..8abb5bb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
-target/
+out/
+.ghpages/
diff --git a/.mill-version b/.mill-version
new file mode 100644
index 0000000..cb0c939
--- /dev/null
+++ b/.mill-version
@@ -0,0 +1 @@
+0.5.2
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 031f3ac..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-sudo: required
-language: scala
-
-jdk:
- - oraclejdk8
-
-before_install:
- - curl https://raw.githubusercontent.com/scala-native/scala-native/master/scripts/travis_setup.sh | bash -x
-
-script:
- - sbt yamlesqueJVM/scalafmtCheck yamlesque-spray-jsonJVM/scalafmtCheck +test
-
-cache:
- directories:
- - "$HOME/.ivy2/cache"
- - "$HOME/.sbt/boot/"
-
-before_cache:
- - find $HOME/.ivy2/cache/io.crashbox -depth -name "yamlesque*" -exec rm -r {} \;
- - find $HOME/.ivy2 -name "ivydata-*.properties" -delete
- - find $HOME/.sbt -name "*.lock" -delete
diff --git a/README.md b/README.md
index cdf5e2c..1583636 100644
--- a/README.md
+++ b/README.md
@@ -1,25 +1,38 @@
-[![Build Status](https://travis-ci.org/jodersky/yamlesque.svg?branch=master)](https://travis-ci.org/jodersky/yamlesque)
-
# yamlesque
Pure Scala YAML parsing.
-As the name suggests, "yam-el-esque" is a Scala implementation of the
-most frequently used YAML features. It takes inspiration from
-Spray-JSON and aims to provide an idiomatic API that is cross-platform
-and has a minimal set of dependencies.
+As the name suggests, "yam-el-esque" is a Scala implementation of the most
+frequently used YAML features. It takes inspiration from [Haoyi Li's
+ujson](http://www.lihaoyi.com/post/uJsonfastflexibleandintuitiveJSONforScala.html)
+and aims to provide an idiomatic API that is cross-platform and has no
+dependencies.
## Getting Started
-Include yamlesque into a project. In sbt, this can be done with:
-```scala
-libraryDependencies += "io.crashbox" %% "yamlesque" % "<latest_tag>"
-```
+Include yamlesque into a project.
-### Parse some YAML
-```scala
-import yamlesque._
+- mill:
+
+ ```scala
+ def ivyDeps = Agg(ivy"io.crashbox::yamlesque::<latest_tag>")
+ ```
+
+- sbt:
+
+ ```scala
+ libraryDependencies += "io.crashbox" %%% "yamlesque" % "<latest_tag>"
+ ```
+**Yamlesque is available for Scala 2.13, 2.12 and 2.11, including ScalaJS and
+Native.**
+
+It should also work with Scala 2.10 and 2.9, although no pre-compiled libraries
+are published for these versions.
+
+### Read Some YAML
+
+```scala
val text = s"""|name: yamlesque
|description: a YAML library for scala
|authors:
@@ -28,27 +41,75 @@ val text = s"""|name: yamlesque
| - name: Another
|""".stripMargin
-// parse yaml to a type safe representation
-val yaml = text.parseYaml
+val yaml: yamlesque.Node = yamlesque.read(text)
+
+val id = yaml.obj("authors").arr(0).obj("id").str
+
+println(id) // == "jodersky"
```
-### Integration with Spray-JSON
-*TODO*
+### Write Some YAML
-### Integration with Akka-HTTP
-*TODO*
+```scala
+import yamlesque.{Arr, Num, Obj, Str}
+val config = Obj(
+ "auth" -> Obj(
+ "username" -> Str("admin"),
+ "password" -> Str("guest")
+ ),
+ "interfaces" -> Arr(
+ Obj(
+ "address" -> Str("0.0.0.0"),
+ "port" -> Num(80)
+ ),
+ Obj(
+ "address" -> Str("0.0.0.0"),
+ "port" -> Num(443)
+ )
+ )
+)
+
+val stringly = yamlesque.write(config)
+
+println(stringly)
+```
+
+will result in
+
+```yaml
+auth:
+ username: admin
+ password: guest
+interfaces:
+ - address: 0.0.0.0
+ port: 80.0
+ - address: 0.0.0.0
+ port: 443.0
+```
-## YAML Conformance
+## Official YAML Conformance
Yamlesque does not strictly implement all features as defined in [YAML
1.2](http://yaml.org/spec/1.2/spec.html), however support should be
-sufficient for most regular documents. Pull requests with additional
-feature implementations are always welcome!
+sufficient for most regular documents.
+
+Available features:
+
+- plain strings (i.e. scalars), including specialization to numbers, booleans
+ and null
+- lists and maps
+- quoted strings
+- comments
+- multiple documents (i.e. ---)
+
+Features which are currently not supported but for which support is planned:
+
+- verbatim blocks (i.e. | and >) (support is limited currently)
+- flow-styles (aka inline JSON)
+
+Unsupported features with no planned implementation:
-The current feature restrictions are:
+- anchors and references
+- tags
-- always assumes utf-8 is used
-- anchors and references are not supported
-- tags are not supported
-- flow-styles (aka inline JSON) aren't supported
-- only single-line literals are allowed (no > or | blocks)
+Pull requests with additional feature implementations are always welcome!
diff --git a/build.sbt b/build.sbt
deleted file mode 100644
index 9d48522..0000000
--- a/build.sbt
+++ /dev/null
@@ -1,50 +0,0 @@
-// shadow sbt-scalajs' crossProject and CrossType until Scala.js 1.0.0 is released
-import sbtcrossproject.{crossProject, CrossType}
-
-val testSettings = Seq(
- libraryDependencies += "com.lihaoyi" %%% "utest" % "0.6.4" % "test",
- testFrameworks += new TestFramework("utest.runner.Framework")
-)
-
-version in ThisBuild := {
- import sys.process._
- ("git describe --always --dirty=-SNAPSHOT --match v[0-9].*" !!).tail.trim
-}
-
-lazy val yamlesque = crossProject(JVMPlatform, JSPlatform, NativePlatform)
- .crossType(CrossType.Pure)
- .settings(testSettings)
- .settings(
- scalaVersion := crossScalaVersions.value.head
- )
- .jvmSettings(
- crossScalaVersions := "2.12.4" :: "2.11.12" :: Nil
- )
- .jsSettings(
- crossScalaVersions := "2.12.4" :: "2.11.12" :: Nil
- )
- .nativeSettings(
- crossScalaVersions := "2.11.12" :: Nil,
- nativeLinkStubs := true // required for utest
- )
-
-lazy val yamlesqueJVM = yamlesque.jvm
-lazy val yamlesqueJS = yamlesque.js
-lazy val yamlesqueNative = yamlesque.native
-
-lazy val `yamlesque-spray-json` = crossProject(JVMPlatform)
- .crossType(CrossType.Pure)
- .dependsOn(yamlesque)
- .settings(testSettings)
- .settings(
- libraryDependencies += "io.spray" %%% "spray-json" % "1.3.4"
- )
-lazy val `yamlesque-spray-jsonJVM` = `yamlesque-spray-json`.jvm
-
-lazy val root = (project in file("."))
- .aggregate(
- yamlesqueJVM,
- yamlesqueJS,
- yamlesqueNative,
- `yamlesque-spray-jsonJVM`
- )
diff --git a/build.sc b/build.sc
new file mode 100644
index 0000000..51b63d8
--- /dev/null
+++ b/build.sc
@@ -0,0 +1,61 @@
+import mill._, scalalib._, scalajslib._, scalanativelib._, scalalib.publish._
+
+val scalaVersions = Map(
+ "2.13" -> "2.13.1",
+ "2.12" -> "2.12.10",
+ "2.11" -> "2.11.12",
+ "2.10" -> "2.10.7"
+)
+
+trait YamlesqueModule extends ScalaModule with PublishModule {
+ def binVersion: String
+ def scalaVersion = scalaVersions(binVersion)
+ def millSourcePath = build.millSourcePath / "yamlesque"
+ def scalacOptions = Seq("-feature", "-deprecation")
+
+ def publishVersion = T.input{os.proc("git", "describe", "--dirty", "--match=v*").call().out.trim.tail}
+ def pomSettings = PomSettings(
+ description = "Simple YAML parsing.",
+ organization = "io.crashbox",
+ url = "https://github.com/jodersky/yamlesque",
+ licenses = Seq(License.`Apache-2.0`),
+ versionControl = VersionControl.github("jodersky", "yamlesque"),
+ developers = Seq(
+ Developer("jodersky", "Jakob Odersky","https://github.com/jodersky")
+ )
+ )
+}
+
+trait YamlesqueTestModule extends TestModule with ScalaModule {
+ def millSourcePath = build.millSourcePath / "yamlesque" / "test"
+ def ivyDeps = Agg(ivy"com.lihaoyi::utest::0.7.1")
+ def testFrameworks = Seq("utest.runner.Framework")
+}
+
+class YamlesqueJVMModule(val binVersion: String) extends YamlesqueModule {
+ object test extends Tests with YamlesqueTestModule
+}
+class YamlesqueJSModule(val binVersion: String) extends YamlesqueModule with ScalaJSModule {
+ def scalaJSVersion = "0.6.29"
+ object test extends Tests with YamlesqueTestModule
+}
+class YamlesqueNativeModule(val binVersion: String) extends YamlesqueModule with ScalaNativeModule {
+ def scalaNativeVersion = "0.3.8"
+ def releaseMode = scalanativelib.api.ReleaseMode.Release
+ object test extends Tests with YamlesqueTestModule
+}
+object yamlesque extends Module {
+ object jvm extends Cross[YamlesqueJVMModule]("2.13", "2.12", "2.11", "2.10")
+ object js extends Cross[YamlesqueJSModule]("2.13", "2.12", "2.11")
+ object native extends Cross[YamlesqueNativeModule]("2.11")
+}
+
+object site extends ScalaJSModule {
+ def scalaVersion = scalaVersions("2.13")
+ def scalaJSVersion = "0.6.29"
+ def moduleDeps = Seq(yamlesque.js("2.13"))
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::ujson::0.8.0",
+ ivy"org.scala-js::scalajs-dom::0.9.7"
+ )
+}
diff --git a/mksite b/mksite
new file mode 100755
index 0000000..a01c0fb
--- /dev/null
+++ b/mksite
@@ -0,0 +1,26 @@
+#!/bin/bash
+# Build an identicon generator website, optionally publish it if
+# "publish" is given as first argument.
+
+set -o errexit
+
+ghpages=.ghpages
+git_url="git@github.com:jodersky/yamlesque.git"
+
+mill site.fullOpt
+mkdir -p "$ghpages"
+cp -f site/index.html "$ghpages"
+cp -f out/site/fullOpt/dest/out.js "$ghpages"
+
+case "$1" in
+ publish)
+ echo "Publishing website" >&2
+ git -C "$ghpages" init
+ git -C "$ghpages" add .
+ git -C "$ghpages" commit -m "Publish website" || true
+ git -C "$ghpages" push -f "$git_url" master:gh-pages
+ ;;
+ *)
+ echo "Skipping publish step" >&2
+ ;;
+esac
diff --git a/project/build.properties b/project/build.properties
deleted file mode 100644
index 0531343..0000000
--- a/project/build.properties
+++ /dev/null
@@ -1 +0,0 @@
-sbt.version=1.1.2
diff --git a/project/plugins.sbt b/project/plugins.sbt
deleted file mode 100644
index 9030e19..0000000
--- a/project/plugins.sbt
+++ /dev/null
@@ -1,9 +0,0 @@
-addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.4.0")
-addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "0.4.0")
-addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.22")
-addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.3.7")
-
-addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "1.4.0")
-
-addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.3")
-addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.1")
diff --git a/publish b/publish
new file mode 100755
index 0000000..bb5e48d
--- /dev/null
+++ b/publish
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+set -o errexit
+
+# Tests are run on a subset of projects
+mill yamlesque.jvm[2.13].test
+mill yamlesque.jvm[2.12].test
+mill yamlesque.js[2.13].test
+mill yamlesque.js[2.12].test
+mill yamlesque.native[2.11].test
+
+# All projects are compiled
+mill yamlesque.jvm[_].compile
+mill yamlesque.js[_].compile
+mill yamlesque.native[_].compile
+
+mill mill.scalalib.PublishModule/publishAll \
+ --sonatypeCreds "8VNUX6+2:$(pass infra/ci-sonatype)" \
+ --publishArtifacts __.publishArtifacts
diff --git a/publish.sbt b/publish.sbt
deleted file mode 100644
index 12101e0..0000000
--- a/publish.sbt
+++ /dev/null
@@ -1,33 +0,0 @@
-organization in ThisBuild := "io.crashbox"
-licenses in ThisBuild := Seq(
- ("Apache 2.0", url("https://www.apache.org/licenses/LICENSE-2.0")))
-homepage in ThisBuild := Some(url("https://github.com/jodersky/yamlesque"))
-publishMavenStyle in ThisBuild := true
-publishTo in ThisBuild := Some(
- if (isSnapshot.value)
- Opts.resolver.sonatypeSnapshots
- else
- Opts.resolver.sonatypeStaging
-)
-scmInfo in ThisBuild := Some(
- ScmInfo(
- url("https://github.com/jodersky/yamlesque"),
- "scm:git@github.com:jodersky/yamlesque.git"
- )
-)
-developers in ThisBuild := List(
- Developer(
- id = "jodersky",
- name = "Jakob Odersky",
- email = "jakob@odersky.com",
- url = url("https://crashbox.io")
- )
-)
-
-// settings for root project
-publishArtifact := false
-publish := {}
-publishLocal := {}
-// make sbt-pgp happy
-publishTo := Some(Resolver.file("Unused transient repository", target.value / "unusedrepo"))
-skip in publish := true
diff --git a/site/index.html b/site/index.html
new file mode 100644
index 0000000..ea0dd74
--- /dev/null
+++ b/site/index.html
@@ -0,0 +1,67 @@
+<!doctype html>
+<html>
+
+<head>
+ <meta charset="UTF-8">
+ <style>
+ * {
+ box-sizing: border-box;
+ }
+
+ html,
+ body {
+ margin: 0;
+ font-family: sans-serif;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: row;
+ align-items: stretch;
+ justify-content: stretch;
+ }
+
+ div {
+ flex: 1;
+ display: flex;
+ flex-direction: row;
+ align-items: stretch;
+ justify-content: stretch;
+
+ border: 1px solid #f1f1f1;
+ border-radius: .25em;
+ padding: 1em;
+ margin: 1em;
+ box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
+ }
+
+ textarea {
+ flex: 1;
+ resize: none;
+ }
+
+ .error {
+ color: red;
+ }
+ </style>
+</head>
+
+<body>
+ <div>
+ <textarea id="input" oninput="generate()" placeholder="write yaml here" oninput="parse()"></textarea>
+ </div>
+
+ <div>
+ <textarea id="output" placeholder="output" readonly></textarea>
+ </div>
+
+ <script type="text/javascript" src="out.js"></script>
+ <script type="text/javascript">
+ function generate() {
+ var str = document.getElementById("input").value
+ yaml.update(str)
+ }
+ </script>
+
+</body>
+
+</html>
diff --git a/site/src/Main.scala b/site/src/Main.scala
new file mode 100644
index 0000000..c009237
--- /dev/null
+++ b/site/src/Main.scala
@@ -0,0 +1,40 @@
+import scala.scalajs.js.annotation._
+import org.scalajs.dom
+
+@JSExportTopLevel("yaml")
+object Main {
+
+ val text: dom.html.TextArea =
+ dom.document.getElementById("output").asInstanceOf[dom.html.TextArea]
+
+ @JSExport
+ def update(str: String): Unit = yamlesque.tryReadAll(str) match {
+ case Left(err) =>
+ text.classList.add("error")
+ text.value = err
+ case Right(yamls) =>
+ text.classList.remove("error")
+ val jsons = yamls.map(ytoj).map(j => ujson.write(j, 2))
+ text.value = jsons.mkString("\n---\n")
+ }
+
+ def ytoj(y: yamlesque.Node): ujson.Value = y match {
+ case yamlesque.Obj(fields) =>
+ val j = ujson.Obj()
+ for ((k, v) <- fields) {
+ j.obj += k -> ytoj(v)
+ }
+ j
+ case yamlesque.Arr(values) =>
+ val j = ujson.Arr()
+ for (v <- values) {
+ j.arr += ytoj(v)
+ }
+ j
+ case yamlesque.Str(x) => ujson.Str(x)
+ case yamlesque.Num(x) => ujson.Num(x)
+ case yamlesque.Bool(x) => ujson.Bool(x)
+ case yamlesque.Null => ujson.Null
+ }
+
+}
diff --git a/yamlesque-spray-json/src/main/scala/formats.scala b/yamlesque-spray-json/src/main/scala/formats.scala
deleted file mode 100644
index 81f86d2..0000000
--- a/yamlesque-spray-json/src/main/scala/formats.scala
+++ /dev/null
@@ -1,44 +0,0 @@
-package yamlesque
-
-import spray.json._
-
-trait JsonYamlFormats {
-
- implicit def jsonToYamlReader[A](
- implicit jsReader: JsonReader[A]): YamlReader[A] = new YamlReader[A] {
- override def read(yaml: YamlValue): A =
- jsReader.read(JsonFormats.yamlToJson(yaml))
- }
-
- implicit def jsonToYamlWriter[A](
- implicit jsWriter: JsonWriter[A]): YamlWriter[A] = new YamlWriter[A] {
- override def write(a: A): YamlValue =
- JsonFormats.jsonToYaml(jsWriter.write(a))
- }
-
-}
-
-object JsonFormats {
-
- def jsonToYaml(js: JsValue): YamlValue = js match {
- case JsNull => YamlEmpty
- case JsNumber(number) => YamlScalar(number.toString)
- case JsBoolean(value) => YamlScalar(value.toString)
- case JsString(value) => YamlScalar(value)
- case JsArray(elements) => YamlSequence(elements.map(jsonToYaml _))
- case JsObject(fields) => YamlMapping(fields.mapValues(jsonToYaml _))
- }
-
- val JsNumberPattern = """([-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?)""".r
-
- def yamlToJson(yaml: YamlValue): JsValue = yaml match {
- case YamlEmpty => JsNull
- case YamlScalar("true") => JsTrue
- case YamlScalar("false") => JsFalse
- case YamlScalar(JsNumberPattern(x)) => JsNumber(x.toDouble)
- case YamlScalar(x) => JsString(x)
- case YamlSequence(elements) => JsArray(elements.map(yamlToJson))
- case YamlMapping(fields) => JsObject(fields.mapValues(yamlToJson))
- }
-
-}
diff --git a/yamlesque-spray-json/src/test/scala/FormatTests.scala b/yamlesque-spray-json/src/test/scala/FormatTests.scala
deleted file mode 100644
index 93d763a..0000000
--- a/yamlesque-spray-json/src/test/scala/FormatTests.scala
+++ /dev/null
@@ -1,53 +0,0 @@
-package yamlesque
-
-import spray.json._
-import utest._
-
-object FormatTests extends TestSuite {
-
- case class A(a1: Int, a2: Seq[B])
- case class B(a: String, b: Option[Boolean])
-
- object Protocol extends DefaultJsonProtocol with JsonYamlFormats {
- implicit def bFormat = jsonFormat2(B)
- implicit def aFormat = jsonFormat2(A)
- }
- import Protocol._
-
- val tests = Tests {
- "parse" - {
- val str =
- s"""|a1: 42
- |a2:
- | - a: hello world
- | b: true
- | - a: yoyo
- |""".stripMargin
-
- "parse yaml" - {
- str.parseYaml ==> YamlMapping(
- "a1" -> YamlScalar("42"),
- "a2" -> YamlSequence(
- YamlMapping(
- "a" -> YamlScalar("hello world"),
- "b" -> YamlScalar("true")
- ),
- YamlMapping(
- "a" -> YamlScalar("yoyo")
- )
- )
- )
- }
- "parse with json readers" - {
- str.parseYaml.convertTo[A] ==> A(
- 42,
- Seq(
- B("hello world", Some(true)),
- B("yoyo", None),
- )
- )
- }
- }
- }
-
-}
diff --git a/yamlesque/src/Parser.scala b/yamlesque/src/Parser.scala
new file mode 100644
index 0000000..6f6c01f
--- /dev/null
+++ b/yamlesque/src/Parser.scala
@@ -0,0 +1,406 @@
+package yamlesque
+import java.io.Reader
+
+trait Tokenizer {
+
+ def in: Reader
+
+ protected sealed trait TokenKind
+ protected case object Key extends TokenKind
+ protected case object Item extends TokenKind
+ protected case object Scalar extends TokenKind
+ protected case object Start extends TokenKind
+ protected case object End extends TokenKind
+
+ protected case object QuotedString extends TokenKind // "", may contain comment
+ protected case object Verbatim extends TokenKind // | or >
+
+ protected val EOF = -1.toChar
+
+ private var line = 1
+ private var col = 0
+ protected var ch: Char = 0
+ private var cr: Boolean = false // was the previous char a carriage return?
+
+ protected var tokenKind: TokenKind = End
+ protected var tokenValue: String = ""
+ protected var tokenLine: Int = 1
+ protected var tokenCol: Int = 1
+
+ private def readChar(): Unit = if (ch != EOF) {
+ ch = in.read().toChar
+ col += 1
+ if (cr) {
+ cr = false
+ line += 1
+ col = 1
+ }
+ if (ch == '\n') {
+ cr = true
+ }
+ }
+ readChar()
+
+ @inline private def accept(c: Char) =
+ if (ch == c) {
+ readChar(); true
+ } else false
+
+ @inline private def skipSpace(): Unit = while (ch == ' ') readChar()
+
+ private val buffer = new StringBuilder
+
+ @inline private def nextStringOrKey() = {
+ var done = false
+ while (!done) {
+ if (accept('\n') || accept(EOF)) {
+ tokenKind = Scalar
+ tokenValue = buffer.result().trim()
+ done = true
+ } else if (accept(' ')) {
+ if (ch == '#') {
+ tokenKind = Scalar
+ tokenValue = buffer.result().trim()
+ done = true
+ } else {
+ buffer += ' '
+ }
+ } else if (accept(':')) {
+ if (accept(' ') || accept('\n') || accept(EOF)) {
+ tokenKind = Key
+ tokenValue = buffer.result().trim()
+ done = true
+ } else {
+ buffer += ':'
+ buffer += ch
+ readChar()
+ }
+ } else {
+ buffer += ch
+ readChar()
+ }
+ }
+ }
+
+ @inline private def nextQuoteOrKey() = {
+ buffer.clear()
+ while (ch != '"' && ch != EOF) {
+ if (accept('\\')) {
+ if (ch != EOF) {
+ buffer += ch
+ readChar()
+ }
+ } else {
+ buffer += ch
+ readChar()
+ }
+ }
+ readChar()
+ tokenValue = buffer.result()
+
+ skipSpace()
+ if (accept(':')) {
+ if (accept(' ') || accept('\n') || accept(EOF)) {
+ tokenKind = Key
+ } else {
+ // this is an irrefular situation and the parser will error out later
+ tokenKind = QuotedString
+ }
+ } else {
+ tokenKind = QuotedString
+ }
+ }
+
+ @annotation.tailrec
+ @inline
+ protected final def nextToken(): Unit = {
+ buffer.clear()
+ skipSpace()
+ if (accept(EOF)) {
+ tokenKind = End
+ tokenLine = line
+ tokenCol = col - 1
+ } else if (accept('#')) {
+ while (ch != '\n' && ch != EOF) {
+ readChar()
+ }
+ nextToken()
+ } else if (accept('\n')) {
+ nextToken()
+ } else if (accept('-')) {
+ tokenLine = line
+ tokenCol = col - 1
+ if (accept('-')) {
+ if (accept('-')) {
+ while (ch != '\n' && ch != EOF) readChar()
+ tokenKind = Start
+ } else {
+ buffer ++= "--"
+ buffer += ch
+ readChar()
+ nextStringOrKey()
+ }
+ } else if (accept(' ') || accept('\n') || accept(EOF)) {
+ tokenKind = Item
+ } else {
+ buffer += '-'
+ buffer += ch
+ readChar()
+ nextStringOrKey()
+ }
+ } else if (ch == '|' || ch == '>') {
+ var marker = ch
+ readChar()
+ if (accept('\n') || accept(EOF)) {
+ nextVerbatimBlock(tokenCol, marker == '>')
+ } else {
+ buffer += marker
+ buffer += ch
+ readChar()
+ nextStringOrKey()
+ }
+ } else if (accept('"')) {
+ tokenLine = line
+ tokenCol = col - 1
+ nextQuoteOrKey()
+ } else {
+ tokenLine = line
+ tokenCol = col
+ nextStringOrKey()
+ }
+ }
+ nextToken()
+
+ protected def nextVerbatimBlock(minCol: Int, foldLines: Boolean) = {
+ buffer.clear()
+ var startCol = 0
+ var lastNonEmptyLine = line
+
+ // find start column, whitespace is significant
+ while (accept('\n')) {
+ buffer += '\n'
+ }
+ skipSpace()
+ startCol = col
+ tokenLine = line
+
+ if (startCol <= minCol) {
+ tokenCol = minCol + 1
+ tokenKind = Verbatim
+ tokenValue = ""
+ } else {
+ var done = false
+ while (!done) {
+ // skip spaces until we reach starting column
+ while (ch == ' ' && col < startCol && ch != EOF) readChar()
+
+ if (ch == '\n') {
+ readChar()
+ done = ch == EOF
+ } else if (col == startCol) {
+ for (i <- lastNonEmptyLine until line - 1) { buffer += '\n' }
+ lastNonEmptyLine = line
+ var eol = false
+ while (!eol) {
+ if (ch == '\n' || ch == EOF) eol = true
+ if (ch != EOF) {
+ buffer += ch
+ readChar()
+ }
+ }
+ done = ch == EOF
+ } else {
+ done = true
+ }
+ }
+
+ tokenKind = Verbatim
+ tokenCol = startCol
+ tokenValue = buffer.result()
+ }
+ }
+
+}
+
+object Parser {
+ case class ParseException(message: String) extends RuntimeException(message)
+}
+
+class Parser(val in: Reader) extends Tokenizer with Iterator[Node] {
+
+ private def friendlyKind(kind: TokenKind) = kind match {
+ case Key => "map key"
+ case Item => "list item"
+ case Scalar => "scalar"
+ case QuotedString => "string"
+ case Verbatim => "verbatim block"
+ case Start => "start of document"
+ case End => "EOF"
+ }
+
+ private def friendlyValue = tokenKind match {
+ case Key => tokenValue + ":"
+ case Item => "-"
+ case Scalar => tokenValue
+ case QuotedString => s""""$tokenValue""""
+ case Verbatim => "verbatim block " + tokenValue.takeWhile(_ != '\n') + "..."
+ case Start => "---"
+ case End => "EOF"
+ }
+
+ private def fatal(message: String): Nothing = {
+ val info = s"$tokenLine:$tokenCol: $message\n"
+ val token = (" " * tokenCol) + friendlyValue + "\n"
+ val caret = (" " * tokenCol) + "^\n"
+ throw new Parser.ParseException(info + token + caret)
+ }
+
+ private var node: Node = null
+
+ // the first document does not strictly need to be started with a ---
+ private def initDocument() = {
+ if (tokenKind == Start) {
+ nextDocument()
+ } else {
+ nextNode()
+ }
+ }
+ initDocument()
+
+ // subsequent documents require an explicit start
+ private def nextDocument() = {
+ tokenKind match {
+ case Start =>
+ nextToken()
+ nextNode()
+ case _ =>
+ fatal(
+ s"expected ${friendlyKind(Start)}, but found ${friendlyKind(tokenKind)}"
+ )
+ }
+ }
+
+ private def nextNode(): Unit = {
+ tokenKind match {
+ case Key => nextMap()
+ case Item => nextList()
+ case Scalar => nextString()
+ case QuotedString =>
+ node = Str(tokenValue)
+ nextToken()
+ case Verbatim =>
+ node = Str(tokenValue)
+ nextToken()
+ case Start | End =>
+ node = Null
+ }
+ }
+
+ private def nextMap(): Unit = {
+ val y = Obj()
+ val startCol = tokenCol
+
+ do {
+ if (tokenKind != Key) {
+ fatal(
+ s"expected ${friendlyKind(Key)}, but found ${friendlyKind(tokenKind)}"
+ )
+ }
+ if (tokenCol != startCol) {
+ fatal(s"${friendlyKind(Key)} is not aligned")
+ }
+
+ val key = tokenValue
+ nextToken()
+
+ tokenKind match {
+ case Start | End =>
+ y.obj(key) = Null
+ // special case: we allow lists to start after a key without requiring an indent
+ case Item if tokenCol == startCol =>
+ nextNode()
+ y.obj(key) = node
+ case _ if tokenCol <= startCol =>
+ y.obj(key) = Null
+ case _ =>
+ nextNode()
+ y.obj(key) = node
+ }
+ } while (tokenCol >= startCol && tokenKind != Start && tokenKind != End)
+ node = y
+ }
+
+ private def nextList(): Unit = {
+ val y = Arr()
+ val startCol = tokenCol
+
+ do {
+ if (tokenKind != Item) {
+ fatal(
+ s"expected ${friendlyKind(Item)}, but found ${friendlyKind(tokenKind)}"
+ )
+ }
+ if (tokenCol != startCol) {
+ fatal(s"${friendlyKind(Item)} is not aligned")
+ }
+
+ nextToken()
+
+ tokenKind match {
+ case Start | End =>
+ y.arr += Null
+ case _ if tokenCol <= startCol =>
+ y.arr += Null
+ case _ =>
+ nextNode()
+ y.arr += node
+ }
+ } while (tokenCol >= startCol && tokenKind != Start && tokenKind != End)
+ node = y
+ }
+
+ private def nextString(): Unit = {
+ val buffer = new StringBuilder
+ val startCol = tokenCol
+
+ buffer ++= tokenValue
+ nextToken()
+
+ while (tokenCol >= startCol && tokenKind != Start && tokenKind != End) {
+ if (tokenKind != Scalar) {
+ fatal(
+ s"expected ${friendlyKind(Scalar)}, but found ${friendlyKind(tokenKind)}"
+ )
+ }
+ buffer += ' '
+ buffer ++= tokenValue
+ nextToken()
+ }
+ node = specializeString(buffer.result())
+ }
+
+ private def specializeString(str: String) = str match {
+ case "null" => Null
+ case "true" => Bool(true)
+ case "false" => Bool(false)
+ case s =>
+ try {
+ Num(s.toDouble)
+ } catch {
+ case _: NumberFormatException => Str(s)
+ }
+ }
+
+ private var reachedEnd = false
+ def hasNext: Boolean = !reachedEnd
+ def next(): Node = {
+ val result = node
+ if (tokenKind == End) {
+ reachedEnd = true
+ } else {
+ nextDocument()
+ }
+ result
+ }
+
+}
diff --git a/yamlesque/src/Writer.scala b/yamlesque/src/Writer.scala
new file mode 100644
index 0000000..e387ef9
--- /dev/null
+++ b/yamlesque/src/Writer.scala
@@ -0,0 +1,74 @@
+package yamlesque
+
+object Writer {
+
+ def write(nodes: Iterable[Node]): String = {
+ val buffer = new StringBuilder
+ write(nodes, buffer)
+ buffer.result()
+ }
+ def write(nodes: Iterable[Node], buffer: StringBuilder): Unit = {
+ val it = nodes.iterator
+ while (it.hasNext) {
+ writeCompact(buffer, true, 0, it.next())
+ if (it.hasNext) buffer ++= "---\n"
+ }
+ }
+
+ private def writeCompact(
+ buffer: StringBuilder,
+ startOfLine: Boolean,
+ indent: Int,
+ node: Node
+ ): Unit = {
+ node match {
+ case Null =>
+ buffer ++= "null\n"
+ case Bool(true) =>
+ buffer ++= "true\n"
+ case Bool(false) =>
+ buffer ++= "false\n"
+ case Num(num) =>
+ buffer ++= num.toString
+ buffer += '\n'
+ case Str(value) =>
+ buffer ++= value
+ buffer += '\n'
+ case Arr(values) =>
+ var doIndent = startOfLine
+ for (item <- values) {
+ if (doIndent) {
+ buffer ++= " " * indent
+ }
+ doIndent = true
+ item match {
+ case Arr(_) =>
+ buffer ++= "-\n"
+ writeCompact(buffer, true, indent + 1, item)
+ case _ =>
+ buffer ++= "- "
+ writeCompact(buffer, false, indent + 1, item)
+ }
+ }
+ case Obj(values) =>
+ var doIndent = startOfLine
+ for ((key, value) <- values) {
+ if (doIndent) {
+ buffer ++= " " * indent
+ }
+ doIndent = true
+
+ buffer ++= key
+ value match {
+ case Str(_) | Bool(_) | Num(_) | Null =>
+ buffer ++= ": "
+ writeCompact(buffer, false, indent, value)
+ case _ =>
+ buffer ++= ":\n"
+ writeCompact(buffer, true, indent + 1, value)
+ }
+ }
+ }
+ }
+
+}
diff --git a/yamlesque/src/YamlNodes.scala b/yamlesque/src/YamlNodes.scala
new file mode 100644
index 0000000..eebe783
--- /dev/null
+++ b/yamlesque/src/YamlNodes.scala
@@ -0,0 +1,81 @@
+package yamlesque
+
+import scala.collection.mutable
+
+sealed trait Node {
+ def isObj: Boolean = false
+ def isArr: Boolean = false
+ def isStr: Boolean = false
+ def isNum: Boolean = false
+ def isBool: Boolean = false
+ def isNull: Boolean = false
+
+ /** Returns the key-value map of this node. Fails if this is not a [[Obj]]. */
+ def obj: mutable.Map[String, Node] = sys.error("not an object")
+ def arr: mutable.ArrayBuffer[Node] = sys.error("not an array")
+ def str: String = sys.error("not a string")
+ def num: Double = sys.error("not a number")
+ def bool: Boolean = sys.error("not a boolean")
+
+}
+object Node {
+ import scala.language.implicitConversions
+ // implicit def SeqToYaml[T](items: IterableOnce[T])
+ // (implicit f: T => Node) = Arr.from(items.map(f))
+ // implicit def JsonableDict[T](items: TraversableOnce[(String, T)])
+ // (implicit f: T => Value)= Obj.from(items.map(x => (x._1, f(x._2))))
+ implicit def StringToYaml(s: CharSequence): Str = Str(s.toString)
+ implicit def ByteToYaml(x: Byte): Num = Num(x)
+ implicit def ShortToYaml(x: Short): Num = Num(x)
+ implicit def IntToYaml(x: Int): Num = Num(x)
+ implicit def LongToYaml(x: Long): Num = Num(x)
+ implicit def FloatToYaml(x: Float): Num = Num(x)
+ implicit def DoubleToYaml(x: Double): Num = Num(x)
+ implicit def BoolToYaml(x: Boolean): Bool = Bool(x)
+ implicit def NullToYaml(x: scala.Null): Null.type = Null
+}
+
+case class Obj(override val obj: mutable.LinkedHashMap[String, Node])
+ extends Node {
+ override def isObj = true
+}
+object Obj {
+ def apply(values: (String, Node)*): Obj = {
+ val builder = mutable.LinkedHashMap.newBuilder[String, Node]
+ builder.sizeHint(values.length)
+ for (v <- values) {
+ builder += v
+ }
+ Obj(builder.result())
+ }
+}
+
+case class Arr(override val arr: mutable.ArrayBuffer[Node]) extends Node {
+ override def isArr = true
+}
+object Arr {
+ def apply(values: Node*): Arr = {
+ val builder = mutable.ArrayBuffer.newBuilder[Node]
+ builder.sizeHint(values.length)
+ for (v <- values) {
+ builder += v
+ }
+ Arr(builder.result())
+ }
+}
+
+case class Str(override val str: String) extends Node {
+ override def isStr: Boolean = true
+}
+
+case class Num(override val num: Double) extends Node {
+ override def isNum: Boolean = true
+}
+
+case class Bool(override val bool: Boolean) extends Node {
+ override def isBool: Boolean = true
+}
+
+case object Null extends Node {
+ override def isNull = true
+}
diff --git a/yamlesque/src/main/scala/YamlParser.scala b/yamlesque/src/main/scala/YamlParser.scala
deleted file mode 100644
index f7a0f9b..0000000
--- a/yamlesque/src/main/scala/YamlParser.scala
+++ /dev/null
@@ -1,258 +0,0 @@
-package yamlesque
-
-import annotation.{switch, tailrec}
-import scala.collection.mutable.ListBuffer
-
-object YamlParser extends (Iterator[Char] => YamlValue) {
-
- sealed trait TokenKind
- object TokenKind {
- case object EOF extends TokenKind
- case object BAD extends TokenKind
- case object DOCSTART extends TokenKind
- case object DOCEND extends TokenKind
- case object MAPPING extends TokenKind
- case object ITEM extends TokenKind
- case object IDENTIFIER extends TokenKind
- case object COMMENT extends TokenKind
- }
- import TokenKind._
-
- case class Token(val kind: TokenKind, value: String = "") {
- var line: Int = 0
- var col: Int = 0
- def setPos(line: Int, col: Int): this.type = {
- this.col = col
- this.line = line
- this
- }
- override def toString() = {
- s"($line, $col): " + super.toString
- }
- }
-
- object Chars {
- final val LF = '\u000A'
- final val CR = '\u000D'
- final val SU = '\u001A'
-
- @inline def isSpace(ch: Char): Boolean = ch match {
- case ' ' | '\t' => true
- case _ => false
- }
-
- @inline def isBlank(ch: Char): Boolean = ch match {
- case ' ' | '\t' | CR | LF | SU => true
- case _ => false
- }
- }
-
- class Scanner(chars: Iterator[Char]) extends Iterator[Token] {
- import Chars._
-
- private var ch0: Char = 0
- private var ch1: Char = 0
- private var ch2: Char = 0
- private var pos: Long = 0
- private var line: Int = 0
- private var col: Int = 0
-
- private def skipChar(): Unit = {
- val ch: Char = if (chars.hasNext) {
- chars.next()
- } else {
- SU
- }
- pos += 1
- col += 1
- ch0 = ch1
- ch1 = ch2
- ch2 = ch
- }
- private def skipChars(n: Int): Unit = {
- var i = 0
- while (i < n) { skipChar(); i += 1 }
- }
- def init() = {
- skipChars(3)
- pos = 0
- col = 0
- line = 0
- }
-
- private var buffer = new StringBuilder()
- private def putChar(): Unit = {
- buffer.append(ch0)
- skipChars(1)
- }
- private def tokenValue(): String = {
- val str = buffer.result()
- buffer.clear()
- str
- }
-
- private var token: Token = Token(BAD, "not yet initialized")
-
- @tailrec private def fetchToken(): Unit = {
- ch0 match {
- case ':' if isBlank(ch1) =>
- token = Token(MAPPING).setPos(line, col)
- skipChars(1)
- case '-' if isBlank(ch1) =>
- token = Token(ITEM).setPos(line, col)
- skipChars(1)
- case '-' if ch1 == '-' && ch2 == '-' =>
- token = Token(DOCSTART).setPos(line, col)
- skipChars(3)
- case '.' if ch1 == '.' && ch2 == '.' =>
- token = Token(DOCEND).setPos(line, col)
- skipChars(3)
- case '#' =>
- val l = line
- val c = col
- skipChars(1)
- while (ch0 != LF && ch0 != SU) {
- putChar()
- }
- token = Token(COMMENT, tokenValue()).setPos(l, c)
- buffer.clear()
- case c if isSpace(c) =>
- skipChars(1)
- fetchToken()
- case LF =>
- skipChars(1)
- col = 0
- line += 1
- fetchToken()
- case CR =>
- skipChars(1)
- if (ch0 == LF) {
- skipChars(1)
- }
- col = 0
- line += 1
- fetchToken()
- case SU =>
- token = Token(EOF).setPos(line, col)
- skipChars(1)
- case _ => fetchScalar()
- }
- }
-
- private def fetchScalar(): Unit = {
- val l = line
- val c = col
- @tailrec def fetchRest(): Unit = ch0 match {
- case ':' if isBlank(ch1) =>
- token = Token(IDENTIFIER, tokenValue())
- case LF =>
- token = Token(IDENTIFIER, tokenValue())
- case SU =>
- token = Token(IDENTIFIER, tokenValue())
- case c =>
- putChar()
- fetchRest()
- }
- fetchRest()
- token.setPos(l, c)
- }
-
- override def hasNext: Boolean = true
- override def next(): Token = {
- fetchToken()
- token
- }
- init()
- }
-
- def parse(tokens: Iterator[Token]): YamlValue = {
- var token0 = tokens.next()
- var token1 = tokens.next()
-
- def readNext(): Unit = {
- token0 = token1
- token1 = tokens.next()
- }
-
- def fatal(message: String, token: Token) = {
- val completeMessage =
- s"parse error at line ${token.line}, column ${token.col}: $message"
- throw new ParseException(completeMessage)
- }
-
- def wrongKind(found: Token, required: TokenKind*) = {
- fatal(
- s"token kind not allowed at this position\n" +
- s" found: ${found.kind}\n" +
- s" required: ${required.mkString(" or ")}\n" +
- " " * found.col + found.value + "\n" +
- " " * found.col + "^",
- found
- )
- }
-
- def nextSequence() = {
- val startCol = token0.col
- val items = new ListBuffer[YamlValue]
- while (startCol <= token0.col && token0.kind != EOF) {
- token0.kind match {
- case ITEM =>
- readNext()
- items += nextBlock(startCol + 1)
- case _ => wrongKind(token0, ITEM)
- }
- }
- YamlSequence(items.toVector)
- }
-
- def nextMapping() = {
- val startCol = token0.col
- val fields = new ListBuffer[(String, YamlValue)]
- while (startCol <= token0.col && token0.kind != EOF) {
- token0.kind match {
- case IDENTIFIER =>
- val key = token0.value
- readNext()
- token0.kind match {
- case MAPPING =>
- readNext()
- val value = nextBlock(startCol + 1)
- fields += key -> value
- case _ => wrongKind(token0, MAPPING)
- }
-
- case _ => wrongKind(token0, IDENTIFIER)
- }
- }
- YamlMapping(fields.toMap)
- }
-
- def nextBlock(startCol: Int): YamlValue = {
- if (token0.col < startCol) {
- YamlEmpty
- } else {
- token0.kind match {
- case IDENTIFIER =>
- if (token1.kind == MAPPING && token0.line == token1.line) {
- nextMapping()
- } else {
- val y = YamlScalar(token0.value)
- readNext()
- y
- }
- case ITEM =>
- nextSequence()
- case EOF => YamlEmpty
- case _ => wrongKind(token0, IDENTIFIER, ITEM)
- }
- }
- }
-
- nextBlock(0)
- }
-
- def apply(data: Iterator[Char]): YamlValue = parse(new Scanner(data))
-
-}
-
-class ParseException(val message: String) extends Exception(message)
diff --git a/yamlesque/src/main/scala/YamlPrinter.scala b/yamlesque/src/main/scala/YamlPrinter.scala
deleted file mode 100644
index 083a8a8..0000000
--- a/yamlesque/src/main/scala/YamlPrinter.scala
+++ /dev/null
@@ -1,48 +0,0 @@
-package yamlesque
-
-import annotation.tailrec
-
-class YamlPrinter(compact: Boolean = true) extends (YamlValue => String) {
-
- def apply(value: YamlValue): String = {
- val str = new StringBuilder()
- def p(value: YamlValue, indentation: Int): Unit = value match {
- case YamlScalar(value) =>
- str ++= " " * indentation
- str ++= value
- str += '\n'
- case YamlSequence(items) =>
- for (item <- items) {
- str ++= " " * indentation
- item match {
- case YamlScalar(v) if compact =>
- str ++= "- "
- str ++= v
- str += '\n'
- case _ =>
- str ++= "-\n"
- p(item, indentation + 1)
- }
- }
- case YamlMapping(fields) =>
- for ((key, value) <- fields) {
- str ++= " " * indentation
- str ++= key
- value match {
- case YamlScalar(v) if compact =>
- str ++= ": "
- str ++= v
- str += '\n'
- case _ =>
- str ++= ":\n"
- p(value, indentation + 1)
- }
- }
- case YamlEmpty =>
- str += '\n'
- }
- p(value, 0)
- str.toString
- }
-
-}
diff --git a/yamlesque/src/main/scala/formats.scala b/yamlesque/src/main/scala/formats.scala
deleted file mode 100644
index 0dbbacc..0000000
--- a/yamlesque/src/main/scala/formats.scala
+++ /dev/null
@@ -1,8 +0,0 @@
-package yamlesque
-
-trait YamlReader[A] {
- def read(yaml: YamlValue): A
-}
-trait YamlWriter[A] {
- def write(a: A): YamlValue
-}
diff --git a/yamlesque/src/main/scala/package.scala b/yamlesque/src/main/scala/package.scala
deleted file mode 100644
index c40ca70..0000000
--- a/yamlesque/src/main/scala/package.scala
+++ /dev/null
@@ -1,26 +0,0 @@
-package yamlesque
-
-object `package` {
-
- def deserializationError(msg: String,
- cause: Throwable = null,
- fieldNames: List[String] = Nil) =
- throw new DeserializationException(msg, cause, fieldNames)
- def serializationError(msg: String) = throw new SerializationException(msg)
-
- implicit class RichAny[A](val any: A) extends AnyVal {
- def toYaml(implicit writer: YamlWriter[A]): YamlValue = writer.write(any)
- }
-
- implicit class RichString(val str: String) extends AnyVal {
- def parseYaml: YamlValue = YamlParser(str.toIterator)
- }
-
-}
-
-case class DeserializationException(msg: String,
- cause: Throwable = null,
- fieldNames: List[String] = Nil)
- extends RuntimeException(msg, cause)
-
-class SerializationException(msg: String) extends RuntimeException(msg)
diff --git a/yamlesque/src/main/scala/yamlValues.scala b/yamlesque/src/main/scala/yamlValues.scala
deleted file mode 100644
index 4432b9d..0000000
--- a/yamlesque/src/main/scala/yamlValues.scala
+++ /dev/null
@@ -1,23 +0,0 @@
-package yamlesque
-
-sealed trait YamlValue {
- def print: String = YamlValue.DefaultPrinter(this)
- def convertTo[A: YamlReader]: A = implicitly[YamlReader[A]].read(this)
-}
-object YamlValue {
- val DefaultPrinter = new YamlPrinter(compact = true)
-}
-
-case class YamlMapping(fields: Map[String, YamlValue]) extends YamlValue
-object YamlMapping {
- def apply(items: (String, YamlValue)*) = new YamlMapping(Map(items: _*))
-}
-
-case class YamlSequence(items: Vector[YamlValue]) extends YamlValue
-object YamlSequence {
- def apply(items: YamlValue*) = new YamlSequence(items.toVector)
-}
-
-case class YamlScalar(value: String) extends YamlValue
-
-case object YamlEmpty extends YamlValue
diff --git a/yamlesque/src/package.scala b/yamlesque/src/package.scala
new file mode 100644
index 0000000..43ab30b
--- /dev/null
+++ b/yamlesque/src/package.scala
@@ -0,0 +1,30 @@
+package object yamlesque {
+ import java.io.StringReader
+
+ def read(input: String): Node = {
+ (new Parser(new StringReader(input))).next()
+ }
+
+ def tryRead(input: String): Either[String, Node] =
+ try {
+ Right(read(input))
+ } catch {
+ case Parser.ParseException(msg) => Left(msg)
+ }
+
+ def readAll(input: String): List[Node] = {
+ (new Parser(new StringReader(input))).toList
+ }
+
+ // TODO: the parser can actually recover from errors when a new document begins
+ def tryReadAll(input: String): Either[String, List[Node]] =
+ try {
+ Right((new Parser(new StringReader(input))).toList)
+ } catch {
+ case Parser.ParseException(msg) => Left(msg)
+ }
+
+ def write(nodes: Node*): String = write(nodes)
+ def write(nodes: Iterable[Node]): String = Writer.write(nodes)
+
+}
diff --git a/yamlesque/src/test/scala/ParserTests.scala b/yamlesque/src/test/scala/ParserTests.scala
deleted file mode 100644
index 9229a14..0000000
--- a/yamlesque/src/test/scala/ParserTests.scala
+++ /dev/null
@@ -1,221 +0,0 @@
-package yamlesque
-
-import utest._
-
-object ParserTests extends TestSuite {
-
- val tests = Tests {
- "parse empty string" - {
- "".parseYaml ==> YamlEmpty
- }
- "parse simple scalar" - {
- "hello".parseYaml ==> YamlScalar("hello")
- }
- "parse scalar with space" - {
- "hello world".parseYaml ==> YamlScalar("hello world")
- }
- "parse scalar with a colon" - {
- "hello:world".parseYaml ==> YamlScalar("hello:world")
- }
- "parse scalar with a minus" - {
- "hello-world".parseYaml ==> YamlScalar("hello-world")
- }
- "parse scalar starting with a colon" - {
- ":hello world".parseYaml ==> YamlScalar(":hello world")
- }
- "parse scalar starting with a minus" - {
- "-hello world".parseYaml ==> YamlScalar("-hello world")
- }
- "parse empty list" - {
- "-".parseYaml ==> YamlSequence(YamlEmpty)
- }
- "parse a simple list" - {
- "-\n a\n-\n b\n-\n c".parseYaml ==> YamlSequence(YamlScalar("a"),
- YamlScalar("b"),
- YamlScalar("c"))
- }
- "parse a simple compact list" - {
- "- a\n- b\n - c".parseYaml ==> YamlSequence(YamlScalar("a"),
- YamlScalar("b"),
- YamlScalar("c"))
- }
- "fail to parse a list with a non-item token" - {
- val e = intercept[ParseException] {
- "- a\n- b\n -c".parseYaml // -c is missing a space between '-' and 'c'
- }
- assert(e.message.contains("token kind"))
- }
- "parse a nested list" - {
- val ls =
- s"""|- a0
- |- b0
- |-
- | - a1
- | - b1
- | -
- | - a2
- | - b2
- |- c0
- |- - a1
- | - b1
- |- - - - a4
- |""".stripMargin
- val result = YamlSequence(
- YamlScalar("a0"),
- YamlScalar("b0"),
- YamlSequence(
- YamlScalar("a1"),
- YamlScalar("b1"),
- YamlSequence(
- YamlScalar("a2"),
- YamlScalar("b2")
- )
- ),
- YamlScalar("c0"),
- YamlSequence(
- YamlScalar("a1"),
- YamlScalar("b1")
- ),
- YamlSequence(
- YamlSequence(
- YamlSequence(
- YamlScalar("a4")
- )
- )
- )
- )
- ls.parseYaml ==> result
- }
- "parse a simple mapping" - {
- "a:\n b".parseYaml ==> YamlMapping("a" -> YamlScalar("b"))
- }
- "parse a double mapping" - {
- "a:\n b\nc:\n d".parseYaml ==> YamlMapping(
- "a" -> YamlScalar("b"),
- "c" -> YamlScalar("d")
- )
- }
- "parse a simple compact mapping" - {
- "a: b".parseYaml ==> YamlMapping("a" -> YamlScalar("b"))
- }
- "parse a double compact mapping" - {
- "a: b\nc: d".parseYaml ==> YamlMapping(
- "a" -> YamlScalar("b"),
- "c" -> YamlScalar("d")
- )
- }
- "parse a simple mapping without a value" - {
- "a:\n".parseYaml ==> YamlMapping(
- "a" -> YamlEmpty
- )
- }
- "parse a mapping without a value" - {
- "k1: v1\nk2:\nk3: v3".parseYaml ==> YamlMapping(
- "k1" -> YamlScalar("v1"),
- "k2" -> YamlEmpty,
- "k3" -> YamlScalar("v3")
- )
- }
- "parse a nested mapping" - {
- val m =
- s"""|k1:
- | k11: a
- | k12: b
- |k2:
- | k21:
- | k31:
- | k41: a
- | k22:
- | b
- |k3: a
- |k4: k41: k42: k43: a
- |""".stripMargin
- m.parseYaml ==> YamlMapping(
- "k1" -> YamlMapping(
- "k11" -> YamlScalar("a"),
- "k12" -> YamlScalar("b")
- ),
- "k2" -> YamlMapping(
- "k21" -> YamlMapping(
- "k31" -> YamlMapping(
- "k41" -> YamlScalar("a")
- )
- ),
- "k22" -> YamlScalar("b")
- ),
- "k3" -> YamlScalar("a"),
- "k4" -> YamlMapping(
- "k41" -> YamlMapping(
- "k42" -> YamlMapping(
- "k43" -> YamlScalar("a")
- )
- )
- )
- )
- }
- "maps and sequences" - {
- val yaml = YamlMapping(
- "key1" -> YamlScalar("value1"),
- "key2" -> YamlMapping(
- "key1" -> YamlScalar("value1"),
- "key2" -> YamlScalar("value1"),
- "key3" -> YamlSequence(
- YamlScalar("a1"),
- YamlSequence(
- YamlScalar("a1"),
- YamlScalar("a2"),
- YamlScalar("a3")
- ),
- YamlScalar("a3"),
- YamlMapping(
- "a1" -> YamlScalar("b"),
- "a2" -> YamlScalar("b"),
- "a3" -> YamlScalar("b"),
- "a4" -> YamlScalar("b")
- ),
- YamlScalar("a4"),
- YamlScalar("a4")
- ),
- "key4" -> YamlScalar("value1"),
- "key5" -> YamlScalar("value1"),
- "key6" -> YamlScalar("value1")
- ),
- "key3" -> YamlScalar("value3")
- )
-
- val string =
- s"""|
- |key1: value1
- |key2:
- | key4:
- | value1
- | key5: value1
- | key1: value1
- | key2: value1
- | key6: value1
- | key3:
- | - a1
- | -
- | - a1
- | - a2
- | - a3
- | - a3
- | -
- | a1: b
- | a2: b
- | a3: b
- | a4: b
- | - a4
- | - a4
- |key3: value3
- |""".stripMargin
- "parse" - {
- string.parseYaml ==> yaml
- }
- "print and parse" - {
- yaml.print.parseYaml ==> yaml
- }
- }
- }
-
-}
diff --git a/yamlesque/test/src/BasicTest.scala b/yamlesque/test/src/BasicTest.scala
new file mode 100644
index 0000000..4261207
--- /dev/null
+++ b/yamlesque/test/src/BasicTest.scala
@@ -0,0 +1,196 @@
+package yamlesque
+
+import utest._
+
+object BasicTest extends TestSuite {
+ def tests = Tests {
+ "empty doc" - {
+ read("") ==> Null
+ }
+ "empty, terminated doc" - {
+ read("---") ==> Null
+ }
+ "null doc" - {
+ read("null") ==> Null
+ }
+ "plain string" - {
+ read("a") ==> Str("a")
+ read("a ") ==> Str("a")
+ }
+ "plain int" - {
+ read("1") ==> Num(1)
+ }
+ "plain double" - {
+ read("1.1") ==> Num(1.1)
+ }
+ "combined plain string" - {
+ read("""|a
+ |b
+ |""".stripMargin) ==> Str("a b")
+ }
+ "combined plain string, indentation" - {
+ read("""|a
+ | b
+ | c
+ |d
+ |""".stripMargin) ==> Str("a b c d")
+ }
+ "plain bool" - {
+ read("true") ==> Bool(true)
+ read("false") ==> Bool(false)
+ }
+ "map, empty" - {
+ read("a: ") ==> Obj("a" -> Null)
+ read("a:") ==> Obj("a" -> Null)
+ read("a:\n") ==> Obj("a" -> Null)
+ }
+ "map, single" - {
+ read("a: b") ==> Obj("a" -> Str("b"))
+ read("a:\n b") ==> Obj("a" -> Str("b"))
+ read("a:\n b") ==> Obj("a" -> Str("b"))
+ }
+ "map, space in key" - {
+ read("a : b") ==> Obj("a" -> Str("b"))
+ read("hello world : b") ==> Obj("hello world" -> Str("b"))
+ }
+ "map, multiple" - {
+ read("""|a: x
+ |b:
+ | y
+ |c:
+ | foo
+ |""".stripMargin) ==> Obj(
+ "a" -> Str("x"),
+ "b" -> Str("y"),
+ "c" -> Str("foo")
+ )
+ }
+ "map, nested" - {
+ read("""|a:
+ | b: x
+ |b: a: foo
+ | b: bar
+ |c: y
+ |""".stripMargin) ==> Obj(
+ "a" -> Obj("b" -> Str("x")),
+ "b" -> Obj(
+ "a" -> Str("foo"),
+ "b" -> Str("bar")
+ ),
+ "c" -> Str("y")
+ )
+ }
+ "list, empty" - {
+ read("- ") ==> Arr(Null)
+ read("-") ==> Arr(Null)
+ read("-\n") ==> Arr(Null)
+ }
+ "list, single" - {
+ read("- a") ==> Arr(Str("a"))
+ read("-\n a") ==> Arr(Str("a"))
+ read("-\n a") ==> Arr(Str("a"))
+ }
+ "list, multiple" - {
+ read("""|- a
+ |-
+ | b
+ |- c
+ |""".stripMargin) ==> Arr(Str("a"), Str("b"), Str("c"))
+ }
+ "list, nested" - {
+ read("""|- a
+ |- - b1
+ | - b2
+ |-
+ | - - c1
+ | - c2
+ |""".stripMargin) ==> Arr(
+ Str("a"),
+ Arr(Str("b1"), Str("b2")),
+ Arr(
+ Arr("c1"),
+ Str("c2")
+ )
+ )
+ }
+ "list after map" - {
+ read("""|a:
+ | - b
+ | - c
+ |""".stripMargin) ==> Obj(
+ "a" -> Arr(Str("b"), Str("c"))
+ )
+ }
+ "list after map, no indent" - {
+ read("""|a:
+ |- b
+ |- c
+ |""".stripMargin) ==> Obj(
+ "a" -> Arr(Str("b"), Str("c"))
+ )
+ }
+ "comment" - {
+ read("#nothing to see here") ==> Null
+ read("# nothing to see here") ==> Null
+ }
+ "comment, after string" - {
+ read("a #nothing to see here") ==> Str("a")
+ }
+ "comment, after key" - {
+ read("a: #nothing to see here") ==> Obj("a" -> Null)
+ }
+ "comment, after item" - {
+ read("- #nothing to see here") ==> Arr(Null)
+ }
+ "not a comment" - {
+ read("a#nothing to see here") ==> Str("a#nothing to see here")
+ read("a:#nothing to see here") ==> Str("a:#nothing to see here")
+ read("a-#nothing to see here") ==> Str("a-#nothing to see here")
+ }
+ "mixed" - {
+ read("""|# Authentication config
+ |auth:
+ | username: john doe
+ | password:
+ | guest
+ | 2fa:
+ | - otp: a1234
+ | -
+ | code: abc
+ | - other: backdoor! # super secret back door
+ |
+ |# Interface to listen on
+ |#
+ |# Multiple are allowed
+ |#
+ |interfaces:
+ | - addr: 0.0.0.0
+ | port: 1234
+ | - addr: 0.0.0.0
+ | port: 80
+ |extra: null
+ |""".stripMargin) ==> Obj(
+ "auth" -> Obj(
+ "username" -> Str("john doe"),
+ "password" -> Str("guest"),
+ "2fa" -> Arr(
+ Obj("otp" -> Str("a1234")),
+ Obj("code" -> Str("abc")),
+ Obj("other" -> Str("backdoor!"))
+ )
+ ),
+ "interfaces" -> Arr(
+ Obj(
+ "addr" -> Str("0.0.0.0"),
+ "port" -> Num(1234)
+ ),
+ Obj(
+ "addr" -> Str("0.0.0.0"),
+ "port" -> Num(80)
+ )
+ ),
+ "extra" -> Null
+ )
+ }
+ }
+}
diff --git a/yamlesque/test/src/NegTest.scala b/yamlesque/test/src/NegTest.scala
new file mode 100644
index 0000000..627ef0d
--- /dev/null
+++ b/yamlesque/test/src/NegTest.scala
@@ -0,0 +1,96 @@
+package yamlesque
+
+import utest._
+
+object NegTest extends TestSuite {
+ def tests = Tests {
+ "key and string" - {
+ val e = intercept[Parser.ParseException] {
+ read("""|b:
+ |a
+ |""".stripMargin)
+ }
+ assert(e.message.contains("expected"))
+ }
+ "list and key" - {
+ val e = intercept[Parser.ParseException] {
+ read("""|- b:
+ |a:
+ |""".stripMargin)
+ }
+ assert(e.message.contains("expected"))
+ }
+ "list and string" - {
+ val e = intercept[Parser.ParseException] {
+ read("""|-
+ |a
+ |""".stripMargin)
+ }
+ assert(e.message.contains("expected"))
+ }
+ "list and key" - {
+ val e = intercept[Parser.ParseException] {
+ read("""|-
+ |a:
+ |""".stripMargin)
+ }
+ assert(e.message.contains("expected"))
+ }
+ "key alignment" - {
+ val e = intercept[Parser.ParseException] {
+ read("""|a:
+ | a:
+ | b:
+ |""".stripMargin)
+ }
+ assert(e.message.contains("aligned"))
+ }
+ "list alignment" - {
+ val e = intercept[Parser.ParseException] {
+ read("""|-
+ | -
+ | -
+ |""".stripMargin)
+ }
+ assert(e.message.contains("aligned"))
+ }
+ "verbatim end" - {
+ val e = intercept[Parser.ParseException] {
+ read("""|a: |
+ | foo
+ | b # b is parsed as a scalar
+ |""".stripMargin)
+ }
+ assert(e.message.contains("expected"))
+ }
+ "verbatim before last token" - {
+ val e = intercept[Parser.ParseException] {
+ read("""|a:
+ | a: |
+ | b
+ |""".stripMargin)
+
+ }
+ assert(e.message.contains("expected"))
+ }
+ "verbatim before last token 2" - {
+ val e = intercept[Parser.ParseException] {
+ read("""|a:
+ | a:
+ | a: |
+ | b:
+ |""".stripMargin)
+ }
+ assert(e.message.contains("aligned"))
+ }
+ "verbatim followed by scalar" - {
+ val e = intercept[Parser.ParseException] {
+ read("""||
+ | a
+ |a
+ |""".stripMargin)
+ }
+ assert(e.message.contains("expected"))
+ }
+ }
+}
diff --git a/yamlesque/test/src/StreamTest.scala b/yamlesque/test/src/StreamTest.scala
new file mode 100644
index 0000000..e739d65
--- /dev/null
+++ b/yamlesque/test/src/StreamTest.scala
@@ -0,0 +1,54 @@
+package yamlesque
+
+import utest._
+import java.io.StringReader
+
+// test multiple documents
+object StreamTest extends TestSuite {
+ def tests = Tests {
+ "empty doc" - {
+ readAll("") ==> Null :: Nil
+ }
+ "empty doc, start only" - {
+ // first --- is optional
+ readAll("---") ==> Null :: Nil
+ }
+ "empty docs" - {
+ readAll("---\n---") ==> Null :: Null :: Nil
+ readAll("---\n---\n---") ==> Null :: Null :: Null :: Nil
+ }
+ "empty and non-empty docs" - {
+ val s = """|---
+ |a
+ |---
+ """.stripMargin
+ readAll(s) ==> Str("a") :: Null :: Nil
+ }
+ "non-empty doc, implicit start" - {
+ val s = """|a
+ |""".stripMargin
+ readAll(s) ==> Str("a") :: Nil
+ }
+ "non-empty doc, explicit start" - {
+ val s = """|---
+ |a
+ |""".stripMargin
+ readAll(s) ==> Str("a") :: Nil
+ }
+ "non-empty docs, implicit start" - {
+ val s = """|a
+ |---
+ |b
+ """.stripMargin
+ readAll(s) ==> Str("a") :: Str("b") :: Nil
+ }
+ "non-empty docs, explicit start" - {
+ val s = """|---
+ |a
+ |---
+ |b
+ """.stripMargin
+ readAll(s) ==> Str("a") :: Str("b") :: Nil
+ }
+ }
+}
diff --git a/yamlesque/test/src/StringTest.scala b/yamlesque/test/src/StringTest.scala
new file mode 100644
index 0000000..b4ae519
--- /dev/null
+++ b/yamlesque/test/src/StringTest.scala
@@ -0,0 +1,33 @@
+package yamlesque
+
+import utest._
+
+object StringTest extends TestSuite {
+ def tests = Tests {
+ "quoted simple" - {
+ read(""""a"""") ==> Str("a")
+ read(""" "a" """) ==> Str("a")
+ }
+ "quoted non-strings" - {
+ read(""""1"""") ==> Str("1")
+ read(""""1.2"""") ==> Str("1.2")
+ read(""""true"""") ==> Str("true")
+ read(""""false"""") ==> Str("false")
+ read(""""null"""") ==> Str("null")
+ }
+ "quoted comment" - {
+ read(""""#hello"""") ==> Str("#hello")
+ read(""""a #hello"""") ==> Str("a #hello")
+ }
+ "scalar with quote" - {
+ read(""" a" """) ==> Str("a\"")
+ read(""" a"hmm" """) ==> Str("a\"hmm\"")
+ read(""" -"a" """) ==> Str("-\"a\"")
+ read(""" :"a" """) ==> Str(":\"a\"")
+ }
+ "quoted key" - {
+ read(""" "a # b": a """) ==> Obj("a # b" -> Str("a"))
+ read(""" "a # b" : a """) ==> Obj("a # b" -> Str("a"))
+ }
+ }
+}
diff --git a/yamlesque/test/src/VerbatimTest.scala b/yamlesque/test/src/VerbatimTest.scala
new file mode 100644
index 0000000..5e0b9d6
--- /dev/null
+++ b/yamlesque/test/src/VerbatimTest.scala
@@ -0,0 +1,114 @@
+package yamlesque
+
+import utest._
+
+object VerbatimTest extends TestSuite {
+ def tests = Tests {
+ "empty verbatim" - {
+ read("|") ==> Str("")
+ read("|\n") ==> Str("")
+ }
+ "single verbatim" - {
+ read("""||
+ | a
+ |""".stripMargin) ==> Str("a\n")
+ read("""||
+ | a
+ |""".stripMargin) ==> Str("a\n")
+ }
+ "multi-line verbatim" - {
+ read("""||
+ | foo bar
+ | baz
+ |""".stripMargin) ==> Str("foo bar\nbaz\n")
+ }
+ "multi-line indent verbatim" - {
+ read("""||
+ | foo
+ | bar
+ | baz
+ |""".stripMargin) ==> Str("foo\n bar\nbaz\n")
+ }
+ "verbatim in map" - {
+ read("""|a: |
+ | foo
+ | bar
+ | baz
+ |b: |
+ | extra
+ | cool!
+ |""".stripMargin) ==> Obj(
+ "a" -> Str("foo\n bar\nbaz\n"),
+ "b" -> Str("extra\ncool!\n")
+ )
+ }
+ "empty verbatim in map" - {
+ read("""|a: |
+ |b:
+ |c: |
+ |d: |""".stripMargin) ==> Obj(
+ "a" -> Str(""),
+ "b" -> Null,
+ "c" -> Str(""),
+ "d" -> Str("")
+ )
+ }
+ "verbatim in list" - {
+ read("""|- |
+ | foo
+ |- |
+ | extra
+ | cool!
+ |""".stripMargin) ==> Arr(Str("foo\n"), Str("extra\ncool!\n"))
+ }
+ "empty verbatim in list" - {
+ read("""|- |
+ |-
+ |- |
+ |- |
+ |""".stripMargin) ==> Arr(Str(""), Null, Str(""), Str(""))
+ }
+ "new lines in verbatim" - {
+ read("""|a: |
+ |
+ | a
+ | b
+ |
+ |
+ | c
+ |
+ |
+ |
+ |b:
+ |""".stripMargin) ==> Obj(
+ "a" -> Str("\na\nb\n\n\nc\n"),
+ "b" -> Null
+ )
+ }
+ "minimum starting col" - {
+ read("""|a:
+ | b: |
+ | c:
+ |""".stripMargin) ==> Obj(
+ "a" -> Obj(
+ "b" -> Str(""),
+ "c" -> Null
+ )
+ )
+ }
+ "minimum starting col prev" - {
+ read("""|a:
+ | a:
+ | a: |
+ | b: foo bar
+ |""".stripMargin) ==> Obj(
+ "a" -> Obj(
+ "a" -> Obj(
+ "a" -> Str("")
+ ),
+ "b" -> Str("foo bar")
+ )
+ )
+ }
+ }
+}