Skip to content

Commit

Permalink
Complete support of parsing multiple documents
Browse files Browse the repository at this point in the history
This adds `parseYAML` and `parseAllYAMLs` to the `yaml` package object.

The first is the single document parsing provided by `.asNode`. The
second parses a sequence of documents.

To implement this, a fairly small addition was made to `Composer`:
`multipleFromEvents`. Which uses `composeNode` to parse `Node`s from the
events so long as there are `tail` events returned.

This also exposes the `pos` method of `Node`. This is useful when
writing custom decoders for nicer error messages.
  • Loading branch information
coreyoconnor committed Dec 23, 2024
1 parent 2da1238 commit 59400f3
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 20 deletions.
2 changes: 1 addition & 1 deletion core/shared/src/main/scala/org/virtuslab/yaml/Node.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import org.virtuslab.yaml.syntax.YamlPrimitive
* ADT that corresponds to the YAML representation graph nodes https://yaml.org/spec/1.2/spec.html#id2764044
*/
sealed trait Node {
private[yaml] def pos: Option[Range]
def pos: Option[Range]
def tag: Tag
def as[T](implicit
c: YamlDecoder[T],
Expand Down
44 changes: 28 additions & 16 deletions core/shared/src/main/scala/org/virtuslab/yaml/Yaml.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,44 +9,56 @@ import org.virtuslab.yaml.internal.load.reader.Tokenizer

package object yaml {

/**
* Parse YAML from the given string.
*/
def parseYAML(str: String): Either[YamlError, Node] =
for {
events <- {
val parser = ParserImpl(Tokenizer.make(str))
parser.getEvents()
}
node <- ComposerImpl.fromEvents(events)
} yield node

/** Parse multiple YAML documents from the given string.
*/
def parseAllYAMLs(str: String): Either[YamlError, List[Node]] =
for {
events <- {
val parser = ParserImpl(Tokenizer.make(str))
parser.getEvents()
}
nodes <- ComposerImpl.multipleFromEvents(events)
} yield nodes

implicit class StringOps(val str: String) extends AnyVal {

/**
* Parse YAML from the given [[String]], returning either [[YamlError]] or [[T]].
*
*
* According to the specification:
* - [[Parser]] takes input string and produces sequence of events
* - then [[Composer]] produces a representation graph from events
* - finally [[YamlDecoder]] (construct phase from the YAML spec) constructs data type [[T]] from the YAML representation.
* - finally [[YamlDecoder]] (construct phase from the YAML spec) constructs data type [[T]] from the YAML representation.
*/
def as[T](implicit
c: YamlDecoder[T],
settings: LoadSettings = LoadSettings.empty
): Either[YamlError, T] =
for {
events <- {
val parser = ParserImpl(Tokenizer.make(str))
parser.getEvents()
}
node <- ComposerImpl.fromEvents(events)
node <- parseYAML(str)
t <- node.as[T]
} yield t

def asNode: Either[YamlError, Node] =
for {
events <- {
val parser = ParserImpl(Tokenizer.make(str))
parser.getEvents()
}
node <- ComposerImpl.fromEvents(events)
} yield node
def asNode: Either[YamlError, Node] = parseYAML(str)
}

implicit class AnyOps[T](val t: T) extends AnyVal {

/**
* Serialize a [[T]] into a YAML string.
*
*
* According to the specification:
* - [[YamlEncoder]] encode type [[T]] into [[Node]]
* - [[Serializer]] serializes [[Node]] into sequence of [[Event]]s
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ import org.virtuslab.yaml.internal.load.parse.NodeEventMetadata
import org.virtuslab.yaml.internal.load.parse.ParserImpl

/**
* Composing takes a series of serialization events and produces a representation graph.
* Composing takes a series of serialization events and produces a representation graph.
* It can fail due to any of several reasons e.g. unexpected event.
* Returns either [[YamlError]] or [[Node]]
* Returns either [[YamlError]] or [[Node]](s)
*/
trait Composer {
def fromEvents(events: List[Event]): Either[YamlError, Node]
def multipleFromEvents(events: List[Event]): Either[YamlError, List[Node]]
}

object ComposerImpl extends Composer {
Expand All @@ -36,6 +37,26 @@ object ComposerImpl extends Composer {
case _ => composeNode(events, mutable.Map.empty).map(_.node)
}

override def multipleFromEvents(events: List[Event]): Either[YamlError, List[Node]] = {
val aliases = mutable.Map.empty[Anchor, Node]

@tailrec
def go(out: List[Node], remaining: List[Event]): Either[YamlError, List[Node]] =
remaining.headOption.map(_.kind) match {
case None | Some(EventKind.StreamEnd) =>
Right(out.reverse)
case Some(_: EventKind.DocumentEnd) =>
go(out, remaining.tail)
case _ =>
composeNode(remaining, aliases) match {
case Right(Result(node, tail)) => go(node :: out, tail)
case Left(error) => Left(error)
}
}

go(List.empty, events)
}

private def composeNode(
events: List[Event],
aliases: mutable.Map[Anchor, Node]
Expand Down
35 changes: 35 additions & 0 deletions core/shared/src/test/scala/org/virtuslab/yaml/ComposerSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,39 @@ class ComposerSuite extends munit.FunSuite {

assertEquals(ComposerImpl.fromEvents(events), expected)
}

test("multiple documents") {
val events = List(
StreamStart,
DocumentStart(),
MappingStart(),
Scalar("k1"),
Scalar("v1"),
MappingEnd,
DocumentEnd(explicit = true),
DocumentStart(explicit = true),
MappingStart(),
Scalar("k2"),
Scalar("v2"),
MappingEnd,
DocumentEnd(explicit = true),
DocumentStart(explicit = true),
MappingStart(),
Scalar("k3"),
Scalar("v3"),
MappingEnd,
DocumentEnd(explicit = true),
StreamEnd
).map(Event(_, None))

val expected = Right(
List(
MappingNode(ScalarNode("k1") -> ScalarNode("v1")),
MappingNode(ScalarNode("k2") -> ScalarNode("v2")),
MappingNode(ScalarNode("k3") -> ScalarNode("v3"))
)
)

assertEquals(ComposerImpl.multipleFromEvents(events), expected)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class DocumentStartEndSpec extends BaseYamlSuite {
assertEquals(yaml.events, Right(expectedEvents))
}

test("implicit document start") {
test("implicit document end") {
val yaml =
s"""|k1: v1
|""".stripMargin
Expand Down

0 comments on commit 59400f3

Please sign in to comment.