toml-scala icon indicating copy to clipboard operation
toml-scala copied to clipboard

Positions for TOML nodes?

Open olafurpg opened this issue 4 years ago • 2 comments

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))

olafurpg avatar Aug 13 '20 07:08 olafurpg

Sidenote: it would be even better to get range positions if possible. It's probably not much more work compared to adding offset positions.

olafurpg avatar Aug 13 '20 07:08 olafurpg

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

olafurpg avatar Aug 13 '20 09:08 olafurpg