Skip to content

Commit

Permalink
Embed: Forbid duplicate keys
Browse files Browse the repository at this point in the history
According to the TOML specification, "defining a key multiple times
is invalid". Return an error if this is the case, including a trace.
  • Loading branch information
tindzk committed Aug 22, 2019
1 parent 3faaa93 commit 6366410
Show file tree
Hide file tree
Showing 11 changed files with 165 additions and 87 deletions.
20 changes: 8 additions & 12 deletions shared/src/main/scala/toml/Codecs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,21 @@ import scala.annotation.implicitNotFound
trait Codec[A] {
def apply(
value: Value,
defaults: Map[String, Any],
defaults: Codec.Defaults,
index: Int
): Either[Codec.Error, A]
): Either[Parse.Error, A]
}

object Codec {
type Field = String
type Address = List[Field]
type Message = String
type Error = (Address, Message)
type Defaults = Map[String, Any]
type Index = Int

def apply[T](
f: (Value, Defaults, Index) => Either[Error, T]
f: (Value, Defaults, Index) => Either[Parse.Error, T]
): Codec[T] = new Codec[T] {
override def apply(
value: Value, defaults: Defaults, index: Index
): Either[Error, T] = f(value, defaults, index)
): Either[Parse.Error, T] = f(value, defaults, index)
}
}

Expand All @@ -45,7 +41,7 @@ trait LowPriorityCodecs {
def f(
head: Option[Value],
tail: Value,
mapError: Codec.Error => Codec.Error,
mapError: Parse.Error => Parse.Error,
default: Option[V],
defaults: Codec.Defaults,
index: Codec.Index
Expand Down Expand Up @@ -96,7 +92,7 @@ trait LowPriorityCodecs {
def f(
head: Value,
tail: Value,
mapError: (Codec.Error, Codec.Index) => Codec.Error,
mapError: (Parse.Error, Codec.Index) => Parse.Error,
defaults: Codec.Defaults,
index: Codec.Index
) =
Expand Down Expand Up @@ -171,7 +167,7 @@ object Codecs extends LowPriorityCodecs with PlatformCodecs {

implicit def listCodec[T](implicit codec: Codec[T]): Codec[List[T]] = Codec {
case (Value.Arr(elems), _, _) =>
elems.zipWithIndex.foldLeft(Right(List.empty): Either[Codec.Error, List[T]]) {
elems.zipWithIndex.foldLeft(Right(List.empty): Either[Parse.Error, List[T]]) {
case (Right(acc), (cur, idx)) =>
codec(cur, Map.empty, 0)
.left.map { case (a, m) => (s"#${idx + 1}" +: a, m) }
Expand All @@ -186,7 +182,7 @@ object Codecs extends LowPriorityCodecs with PlatformCodecs {
implicit def tableCodec[T](implicit codec: Codec[T]): Codec[Map[String, T]] =
Codec {
case (Value.Tbl(value), _, _) =>
value.foldLeft(Right(Map.empty): Either[Codec.Error, Map[String, T]]) {
value.foldLeft(Right(Map.empty): Either[Parse.Error, Map[String, T]]) {
case (Left(l), _) => Left(l)
case (Right(r), (k, v)) =>
codec(v, Map.empty, 0) match {
Expand Down
87 changes: 65 additions & 22 deletions shared/src/main/scala/toml/Embed.scala
Original file line number Diff line number Diff line change
@@ -1,56 +1,99 @@
package toml

object Embed {
def mergeValues(stack: List[String],
result: Map[String, Value],
insert: (String, Value)
): Either[Parse.Error, Map[String, Value]] = {
val (key, value) = insert
if (result.contains(key)) Left((stack :+ key) -> "Cannot redefine value")
else Right(result + (key -> value))
}

def mergeTable(insert: List[(String, Value)],
trace: List[String]
): Either[Parse.Error, Value.Tbl] =
merge(insert)(Value.Tbl(Map())) {
case (result, cur) =>
mergeValues(trace, result.values, cur)
.right
.map(values => result.copy(values = values))
}

def merge[T, U]
(values: List[T])
(init: U)
(f: (U, T) => Either[Parse.Error, U]): Either[Parse.Error, U] =
{
values.foldLeft(Right(init): Either[Parse.Error, U]) {
case (Left(m), _) => Left(m)
case (Right(result), node) => f(result, node)
}
}

def updateTable(value: Value.Tbl,
stack: List[String],
insert: Map[String, Value]): Value.Tbl =
trace: List[String],
insert: List[(String, Value)]
): Either[Parse.Error, Value.Tbl] =
stack match {
case Nil => Value.Tbl(insert)
case Nil => mergeTable(insert, trace)

case stackHead :: stackTail =>
val child =
value.values.get(stackHead) match {
case Some(v: Value.Tbl) => updateTable(v, stackTail, insert)
case Some(v: Value.Tbl) => updateTable(v, stackTail, trace, insert)
case Some(Value.Arr(init :+ last)) =>
Value.Arr(init :+ updateTable(
last.asInstanceOf[Value.Tbl], stackTail, insert))
case None => updateTable(Value.Tbl(Map.empty), stackTail, insert)
updateTable(
last.asInstanceOf[Value.Tbl], stackTail, trace, insert
).right.map(v => Value.Arr(init :+ v))
case None => updateTable(Value.Tbl(Map()), stackTail, trace, insert)
}

value.copy(values = value.values + (stackHead -> child))
child.right.map(c =>
value.copy(values = value.values + (stackHead -> c)))
}

def addArrayRow(value: Value,
stack: List[String],
insert: Map[String, Value]): Value =
trace: List[String],
insert: List[(String, Value)]): Either[Parse.Error, Value] =
stack match {
case Nil =>
value match {
case v: Value.Arr => v.copy(values = v.values :+ Value.Tbl(insert))
case _: Value.Tbl => Value.Arr(List(Value.Tbl(insert)))
}
mergeTable(insert, trace).right.map(tbl =>
value match {
case v: Value.Arr => v.copy(values = v.values :+ tbl)
case _: Value.Tbl => Value.Arr(List(tbl))
})

case stackHead :: stackTail =>
value match {
case Value.Arr(init :+ last) =>
Value.Arr(init :+ addArrayRow(last, stack, insert))
addArrayRow(last, stack, trace, insert)
.right
.map(v => Value.Arr(init :+ v))

case v: Value.Tbl =>
val oldChild = v.values.getOrElse(stackHead, Value.Tbl(Map.empty))
val newChild = addArrayRow(oldChild, stackTail, insert)

v.copy(values = v.values + (stackHead -> newChild))
val oldChild = v.values.getOrElse(stackHead, Value.Tbl(Map()))
addArrayRow(oldChild, stackTail, trace, insert).right.map(
newChild => v.copy(values = v.values + (stackHead -> newChild)))
}
}

/** Eliminates all [[Node]] instances, converting them to tables and arrays */
def root(root: Root): Value.Tbl =
root.nodes.foldLeft(Value.Tbl(Map.empty)) { (result, node) =>
def root(root: Root): Either[Parse.Error, Value.Tbl] =
merge(root.nodes)(Value.Tbl(Map())) { case (result, node) =>
node match {
case Node.Pair(key, value) =>
result.copy(values = result.values + (key -> value))
mergeValues(List(), result.values, key -> value)
.right
.map(values => result.copy(values = values))
case Node.NamedArray(path, pairs) =>
addArrayRow(result, path, pairs).asInstanceOf[Value.Tbl]
case Node.NamedTable(path, pairs) => updateTable(result, path, pairs)
addArrayRow(result, path, path, pairs)
.right
.map(_.asInstanceOf[Value.Tbl])
case Node.NamedTable(path, pairs) =>
updateTable(result, path, path, pairs)
}
}
}
8 changes: 2 additions & 6 deletions shared/src/main/scala/toml/Generate.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,10 @@ object Generate {
case Node.Pair(k, v) => k + " = " + generate(v)
case Node.NamedTable(ref, values) =>
"[" + generateRef(ref) + "]\n" +
values.mapValues(generate(_)).map { case (k, v) =>
k + " = " + v
}.mkString("\n")
values.map { case (k, v) => k + " = " + generate(v) }.mkString("\n")
case Node.NamedArray(ref, values) =>
"[[" + generateRef(ref) + "]]\n" +
values.mapValues(generate(_)).map { case (k, v) =>
k + " = " + v
}.mkString("\n")
values.map { case (k, v) => k + " = " + generate(v) }.mkString("\n")
}

def generate(root: Root): String =
Expand Down
4 changes: 2 additions & 2 deletions shared/src/main/scala/toml/Node.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ sealed trait Node
object Node {
case class Pair(key: String, value: Value) extends Node

case class NamedTable(ref: List[String], values: Map[String, Value])
case class NamedTable(ref: List[String], values: List[(String, Value)])
extends Node

/** Reference to an array item */
case class NamedArray(ref: List[String], values: Map[String, Value])
case class NamedArray(ref: List[String], values: List[(String, Value)])
extends Node
}

Expand Down
8 changes: 8 additions & 0 deletions shared/src/main/scala/toml/Parse.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package toml

object Parse {
type Field = String
type Address = List[Field]
type Message = String
type Error = (Address, Message)
}
4 changes: 2 additions & 2 deletions shared/src/main/scala/toml/Rules.scala
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,11 @@ object Rules extends PlatformRules {
val pairNode: Parser[Node.Pair] = pair.map { case (k, v) => Node.Pair(k, v) }
val table: Parser[Node.NamedTable] =
P(skip ~ tableDef ~ skip ~ pair.rep(sep = skip)).map { case (a, b) =>
Node.NamedTable(a.toList, b.toMap)
Node.NamedTable(a.toList, b.toList)
}
val tableArray: Parser[Node.NamedArray] =
P(skip ~ tableArrayDef ~ skip ~ pair.rep(sep = skip)).map { case (a, b) =>
Node.NamedArray(a.toList, b.toMap)
Node.NamedArray(a.toList, b.toList)
}

lazy val elem: Parser[Value] = P {
Expand Down
22 changes: 10 additions & 12 deletions shared/src/main/scala/toml/Toml.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import shapeless._
import scala.meta.internal.fastparse.core.Parsed._

object Toml {
def parse(toml: String): Either[String, Value.Tbl] =
def parse(toml: String): Either[Parse.Error, Value.Tbl] =
Rules.root.parse(toml) match {
case Success(v, _) => Right(Embed.root(v))
case f: Failure[_, _] => Left(f.msg)
case Success(v, _) => Embed.root(v)
case f: Failure[_, _] => Left(List() -> f.msg)
}

def generate(root: Root): String = Generate.generate(root)
Expand All @@ -19,7 +19,7 @@ object Toml {
defaults : Default.AsRecord.Aux[A, D],
defaultMapper: util.RecordToMap[D],
codec : Codec[R]
): Either[Codec.Error, A] = {
): Either[Parse.Error, A] = {
val d = defaultMapper(defaults())
codec(table, d, 0).right.map(generic.from)
}
Expand All @@ -29,20 +29,18 @@ object Toml {
defaults : Default.AsRecord.Aux[A, D],
defaultMapper: util.RecordToMap[D],
codec : Codec[R]
): Either[Codec.Error, A] = {
): Either[Parse.Error, A] = {
val d = defaultMapper(defaults())
parse(toml)
.left.map((List.empty, _))
.right.flatMap(codec(_, d, 0).right.map(generic.from))
parse(toml).right.flatMap(codec(_, d, 0).right.map(generic.from))
}
}

class CodecHelperValue[A] {
def apply(value: Value)(implicit codec: Codec[A]): Either[Codec.Error, A] =
codec(value, Map.empty, 0)
def apply(value: Value)(implicit codec: Codec[A]): Either[Parse.Error, A] =
codec(value, Map(), 0)

def apply(toml: String)(implicit codec: Codec[A]): Either[Codec.Error, A] =
parse(toml).left.map((List.empty, _)).right.flatMap(codec(_, Map.empty, 0))
def apply(toml: String)(implicit codec: Codec[A]): Either[Parse.Error, A] =
parse(toml).right.flatMap(codec(_, Map(), 0))
}

def parseAs [T]: CodecHelperGeneric[T] = new CodecHelperGeneric[T]
Expand Down
Loading

0 comments on commit 6366410

Please sign in to comment.