Skip to content

Commit

Permalink
Add API for modify Node (#99)
Browse files Browse the repository at this point in the history
* Add API for modify Node
  • Loading branch information
lwronski authored Nov 16, 2021
1 parent da4a50c commit 9dd1f18
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 0 deletions.
12 changes: 12 additions & 0 deletions yaml/shared/src/main/scala/org/virtuslab/yaml/Node.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.virtuslab.yaml

import org.virtuslab.yaml.Range
import org.virtuslab.yaml.Tag
import org.virtuslab.yaml.syntax.NodeSelector
import org.virtuslab.yaml.syntax.YamlPrimitive

/**
Expand All @@ -16,6 +17,10 @@ sealed trait Node:
): Either[YamlError, T] =
c.construct(this)

def modify(index: Int): NodeVisitor = NodeVisitor(this, List(NodeSelector.IntSelector(index)))
def modify(field: String): NodeVisitor =
NodeVisitor(this, List(NodeSelector.StringSelector(field)))

object Node:
final case class ScalarNode private[yaml] (value: String, tag: Tag, pos: Option[Range] = None)
extends Node
Expand Down Expand Up @@ -55,4 +60,11 @@ object Node:
new MappingNode(mappings, Tag.map, None)
def unapply(node: MappingNode): Option[(Map[Node, Node], Tag)] = Some((node.mappings, node.tag))
end MappingNode

extension (either: Either[TraverseError, Node])
def modify(index: Int): Either[TraverseError, NodeVisitor] = either.map(_.modify(index))

extension (either: Either[TraverseError, Node])
def modify(field: String): Either[TraverseError, NodeVisitor] = either.map(_.modify(field))

end Node
103 changes: 103 additions & 0 deletions yaml/shared/src/main/scala/org/virtuslab/yaml/NodeVisitor.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package org.virtuslab.yaml

import org.virtuslab.yaml.Node
import org.virtuslab.yaml.Node.*
import org.virtuslab.yaml.Range
import org.virtuslab.yaml.TraverseError
import org.virtuslab.yaml.YamlError
import org.virtuslab.yaml.syntax.NodeSelector
import org.virtuslab.yaml.syntax.NodeSelector.*

class NodeVisitor(node: Node, selectors: List[NodeSelector]):
def apply(index: Int): NodeVisitor =
NodeVisitor(node, selectors :+ NodeSelector.IntSelector(index))
def apply(field: String): NodeVisitor =
NodeVisitor(node, selectors :+ NodeSelector.StringSelector(field))

private def updateScalarNode(
value: String,
scalar: ScalarNode
): Either[TraverseError, ScalarNode] =
selectors match {
case Nil =>
Right(
scalar.copy(
value = value
)
)
case _ =>
Left(
TraverseError(
s"Expected end of scalar path, instead found path ${selectors.map(_.show).mkString(".")}"
)
)
}

private def updateSequenceNode(
value: String,
sequence: SequenceNode
): Either[TraverseError, SequenceNode] =
selectors match {
case IntSelector(index) :: rest =>
val nodes = sequence.nodes
val updateNode = NodeVisitor(nodes(index), rest).setValue(value)
updateNode.map(node => sequence.copy(nodes = nodes.updated(index, node)))

case StringSelector(field) :: rest =>
Left(
TraverseError(
s"Expeceted index of sequence, insted found string path: ${field}"
)
)
case _ => Left(TraverseError(s"Expeceted index of sequence, insted found end of path"))
}

private def updateMappingNode(value: String, mapping: MappingNode) =
selectors match
case StringSelector(field) :: rest =>
val mappings = mapping.mappings
val entryToUpdateOpt = mappings.find {
case (ScalarNode(keyName, _), _) => keyName == field
case _ => false
}

entryToUpdateOpt match
case Some(entryToUpdate) =>
val updatedValueE = entryToUpdate match
case (ScalarNode(keyName, _), valueNode) =>
val updatedNode = NodeVisitor(valueNode, rest).setValue(value)
updatedNode
case _ => Left(TraverseError(s"Not found $field in mapping"))

updatedValueE.map { updatedValue =>
mapping.copy(
mappings.updated(entryToUpdate._1, updatedValue)
)
}
case None => Left(TraverseError(s"Not found $field in mapping"))
case IntSelector(index) :: rest =>
Left(
TraverseError(
s"Expeceted plain test, insted found index: $index"
)
)
case _ => Left(TraverseError(s"Expeceted plain text, insted found end of path"))

def setValue(value: String): Either[TraverseError, Node] = node match
case scalar: ScalarNode => updateScalarNode(value, scalar)
case sequence: SequenceNode => updateSequenceNode(value, sequence)
case mapping: MappingNode => updateMappingNode(value, mapping)

object NodeVisitor:

def apply(node: Node, selectors: List[NodeSelector]): NodeVisitor =
new NodeVisitor(node, selectors)

extension (either: Either[TraverseError, NodeVisitor])
def apply(field: String): Either[TraverseError, NodeVisitor] = either.map(_.apply(field))

extension (either: Either[TraverseError, NodeVisitor])
def apply(index: Int): Either[TraverseError, NodeVisitor] = either.map(_.apply(index))

extension (either: Either[TraverseError, NodeVisitor])
def setValue(value: String): Either[TraverseError, Node] = either.flatMap(_.setValue(value))
9 changes: 9 additions & 0 deletions yaml/shared/src/main/scala/org/virtuslab/yaml/Yaml.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ extension (str: String)
t <- node.as[T]
yield t

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

extension [T](t: T)
/**
* Serialize a [[T]] into a YAML string.
Expand Down
2 changes: 2 additions & 0 deletions yaml/shared/src/main/scala/org/virtuslab/yaml/YamlError.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ object ParseError:

final case class ComposerError(msg: String) extends YamlError

final case class TraverseError(msg: String) extends YamlError

final case class ConstructError(msg: String) extends YamlError
object ConstructError:
private def from(errorMsg: String, node: Option[Node], expected: Option[String]): ConstructError =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.virtuslab.yaml.syntax

import org.virtuslab.yaml.Node

sealed trait NodeSelector:
def show: String

object NodeSelector:

case class IntSelector(index: Int) extends NodeSelector:
override def show: String = index.toString
case class StringSelector(field: String) extends NodeSelector:
override def show: String = field
11 changes: 11 additions & 0 deletions yaml/shared/src/test/scala/org/virtuslab/yaml/TestOps.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.virtuslab.yaml

object TestOps {

extension [E <: YamlError, T](either: Either[E, T])
def orThrow: T =
either match
case Left(e) => throw new RuntimeException(e.msg)
case Right(t) => t

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.virtuslab.yaml.travers

import org.virtuslab.yaml.*
import org.virtuslab.yaml.TestOps.*
import org.virtuslab.yaml.Node
import org.virtuslab.yaml.NodeVisitor._

class NodeVisitorSuite extends munit.FunSuite {

test("should update ports for web") {

val yaml =
s"""version: "3.9"
|services:
| web:
| build: .
| volumes:
| - .:/code
| - logvolume01:/var/log
| ports:
| - "5000:5000"
| redis:
| image: "redis:alpine"
|""".stripMargin

val node: Node = yaml.asNode.orThrow
val modifiedNode: Node =
node
.modify("services")("web")("ports")(0)
.setValue("6000:6000")
.modify("services")("redis")("image")
.setValue("openjdk:11")
.orThrow

val modifiedYaml = modifiedNode.asYaml

val exptectedYaml =
s"""version: 3.9
|services:
| web:
| build: .
| volumes:
| - .:/code
| - logvolume01:/var/log
| ports:
| - 6000:6000
| redis:
| image: openjdk:11
|""".stripMargin

assertEquals(modifiedYaml, exptectedYaml)
}
}

0 comments on commit 9dd1f18

Please sign in to comment.