toml-scala
toml-scala copied to clipboard
Positions for TOML nodes?
Thank you for creating this project! I'm using toml-scala in new configuration library that I'm working on (adaptation of https://github.com/scalameta/metaconfig) and I'm curious if it would be possible to somehow expose position information in the toml.Value._
case classes.
My goal is to be able to report positioned type error messages when decoding TOML into Scala case classes, for example
# config.toml
booleanField = "stringValue"
The reported error would look like this
config.toml:0 error: Type mismatch;
found : String
expected : Boolean
booleanField = "stringValue"
^
One way to achieve this with an immutable ADT with minimal boilerplate is to add a private var into the superclass
abstract class Value {
private var myIndex: Option[Int] = None
def position: Option[Int] = myIndex
def withPosition(newIndex: Int): Value = {
val copy = copyThis()
copy.myIndex = Some(newIndex)
copy
}
private[this] def copyThis(): Value = this match {
case Value.Str(value) => Value.Str(value)
// ...
}
}
As long as copyThis().myIndex =
is the only place where you mutate the var then the class remains effectively immutable. The position field can be added in a backwards compatible way, it doesn't affect Value
equality semantics.
The Rules
parser can then construct nodes with the Index
fastparse parser and construct nodes with something like this (I didn't check if it compiles)
val basicStr: Parser[Value.Str] =
P(Index ~ (DoubleQuote.toString ~/ (strChars | escape).rep.! ~ DoubleQuote.toString))
.map((index, str) => Value.Str(Unescape.unescapeJavaString(str)).withPosition(index))
Sidenote: it would be even better to get range positions if possible. It's probably not much more work compared to adding offset positions.
Another alternative solution would be to add a new visitor API to construct TOML nodes, which the parser would use instead of constructing toml.Value._
nodes directly.
class TomlVisitor[T] {
def visitString(value: String, startOffset: Int, endOffset: Int): T
// ...
}
class ValueTomlVisitor[toml.Value] extends TomlVistor[toml.Value] {
def visitString(value: String, startOffset: Int, endOffset: Int) = toml.Value.Str(value)
// ...
}
class Rules[T](visitor: TomlVisitor[T]) {
val root: T = ...
}
Similar to https://github.com/typelevel/jawn
Related blog post https://www.lihaoyi.com/post/ZeroOverheadTreeProcessingwiththeVisitorPattern.html