Skip to content

Commit

Permalink
Merge pull request #15 from sparsetech/feat/language-extensions
Browse files Browse the repository at this point in the history
Introduce language extension for multi-line inline tables
  • Loading branch information
tindzk committed Aug 28, 2019
2 parents bef7ade + 10a891e commit d0d988d
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 52 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,20 @@ scalaDeps = [
]
```

#### Language Extensions
toml-scala supports the following language extensions which are disabled by default:

* [New lines and trailing commas in inline tables](https://github.com/toml-lang/toml/issues/516)

To enable them, pass in a set of extensions to the `parse()` or `parseAs()` function as a second argument:

```scala
toml.Toml.parse("""key = {
a = 23,
b = 42,
}""", Set(toml.Extension.MultiLineInlineTables))
```

## Links
* [ScalaDoc](https://www.javadoc.io/doc/tech.sparse/toml-scala_2.12/)

Expand Down
4 changes: 2 additions & 2 deletions jvm/src/main/scala/toml/PlatformRules.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import java.time._

import scala.meta.internal.fastparse.all._

trait PlatformRules { this: Rules.type =>
trait PlatformRules { this: Rules =>
private val TenPowers =
List(1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000)

Expand Down Expand Up @@ -39,4 +39,4 @@ trait PlatformRules { this: Rules.type =>
}

val date = P(offsetDateTime | localDateTime | localDate | localTime)
}
}
65 changes: 34 additions & 31 deletions shared/src/main/scala/toml/Rules.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,31 @@ private[toml] case class NamedFunction[T, V](f: T => V, name: String)
override def toString: String = name
}

object Rules extends PlatformRules {
sealed trait Extension
object Extension {
case object MultiLineInlineTables extends Extension
}

case object Rules extends toml.Rules(Set())

class Rules(extensions: Set[Extension]) extends PlatformRules {
import Constants._
import Extension._

val UntilNewline = NamedFunction(!CrLf.contains(_: Char), "UntilNewline")

val newLine = P(StringIn(CrLf, Lf))
val charsChunk = P(CharsWhile(UntilNewline))
val comment = P("#" ~ charsChunk.? ~ &(newLine | End))
val whitespace = P(CharIn(WhitespaceChars.toList))

val whitespace = P(CharIn(WhitespaceChars.toList))
val whitespaces = P(whitespace.rep(1))
val skip = P(NoCut(NoTrace((whitespace | comment | newLine).rep)))
val skipWs = P(NoCut(NoTrace(whitespace.rep)))

val skip = P(NoCut(NoTrace((whitespaces | comment | newLine).rep)))

val letter = P(CharIn(LettersRange))
val letters = P(letter.rep(1))
val digit = P(CharIn(NumbersRange))
val digits = P(digit.rep(1))

val skipSpaces = P(CharsWhile(_.isWhitespace).?)
val letter = P(CharIn(LettersRange))
val digit = P(CharIn(NumbersRange))
val digits = P(digit.rep(1))
val dash = P(CharIn(Dashes.toList))

val StringChars = NamedFunction(!"\"\\".contains(_: Char), "StringChars")
val strChars = P(CharsWhile(StringChars))
Expand All @@ -52,14 +57,14 @@ object Rules extends PlatformRules {
val multiLineBasicStr: Parser[Value.Str] =
P(
MultiLineDoubleQuote ~/
skipSpaces ~
newLine.? ~
(!MultiLineDoubleQuote ~ AnyChar).rep.! ~
MultiLineDoubleQuote
).map(str => Value.Str(Unescape.unescapeJavaString(str)))
val multiLineLiteralStr: Parser[Value.Str] =
P(
MultiLineSingleQuote ~/
skipSpaces ~
newLine.? ~
(!MultiLineSingleQuote ~ AnyChar).rep.! ~
MultiLineSingleQuote
).map(Value.Str)
Expand Down Expand Up @@ -98,44 +103,42 @@ object Rules extends PlatformRules {
val `false` = P("false").map(_ => Value.Bool(false))
val boolean = P(`true` | `false`)

val dashes = P(CharIn(Dashes.toList))
val bareKey = P((letters | digits | dashes).rep(min = 1)).!
val bareKey = P((letter | digit | dash).rep(min = 1)).!
val validKey: Parser[String] =
P(NoCut(basicStr.map(_.value)) | NoCut(literalStr.map(_.value)) | bareKey)
val pair: Parser[(String, Value)] =
P(validKey ~ whitespaces.? ~ "=" ~ whitespaces.? ~ elem)
P(validKey ~ skipWs ~ "=" ~ skipWs ~ elem)
val array: Parser[Value.Arr] =
P("[" ~ skip ~ elem.rep(sep = "," ~ skip) ~ ",".? ~ skip ~ "]")
.map(l => Value.Arr(l.toList))
val inlineTable: Parser[Value.Tbl] =
P("{" ~ skip ~ pair.rep(sep = "," ~ skip) ~ ",".? ~ skip ~ "}")
.map(p => Value.Tbl(p.toMap))
(if (extensions.contains(MultiLineInlineTables))
P("{" ~ skip ~ pair.rep(sep = "," ~ skip) ~ ",".? ~ skip ~ "}")
else
P("{" ~ skipWs ~ pair.rep(sep = "," ~ skipWs) ~ skipWs ~ "}")
).map(p => Value.Tbl(p.toMap))

val tableIds: Parser[Seq[String]] =
P(validKey.rep(min = 1, sep = whitespaces.? ~ "." ~ whitespaces.?).map(_.toSeq))
P(validKey.rep(min = 1, sep = skipWs ~ "." ~ skipWs).map(_.toSeq))
val tableDef: Parser[Seq[String]] =
P("[" ~ whitespaces.? ~ tableIds ~ whitespaces.? ~ "]")
P("[" ~ skipWs ~ tableIds ~ skipWs ~ "]")
val tableArrayDef: Parser[Seq[String]] =
P("[[" ~ whitespaces.? ~ tableIds ~ whitespaces.? ~ "]]")
P("[[" ~ skipWs ~ tableIds ~ skipWs ~ "]]")

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) =>
P(tableDef ~ skip ~ pair.rep(sep = skip)).map { case (a, b) =>
Node.NamedTable(a.toList, b.toList)
}
val tableArray: Parser[Node.NamedArray] =
P(skip ~ tableArrayDef ~ skip ~ pair.rep(sep = skip)).map { case (a, b) =>
P(tableArrayDef ~ skip ~ pair.rep(sep = skip)).map { case (a, b) =>
Node.NamedArray(a.toList, b.toList)
}

lazy val elem: Parser[Value] = P {
skip ~
(date | string | boolean | double | integer | array | inlineTable) ~
skip
}

lazy val node: Parser[Node] = P(skip ~ (pairNode | table | tableArray))
lazy val elem: Parser[Value] =
P(date | string | boolean | double | integer | array | inlineTable)

val root: Parser[Root] = P(node.rep(sep = skip) ~ skip ~ End)
val node: Parser[Node] = P(pairNode | table | tableArray)
val root: Parser[Root] = P(skip ~ node.rep(sep = skip) ~ skip ~ End)
.map(nodes => Root(nodes.toList))
}
26 changes: 20 additions & 6 deletions shared/src/main/scala/toml/Toml.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import shapeless._
import scala.meta.internal.fastparse.core.Parsed._

object Toml {
def parse(toml: String): Either[Parse.Error, Value.Tbl] =
Rules.root.parse(toml) match {
def parse(toml: String, extensions: Set[Extension] = Set()): Either[Parse.Error, Value.Tbl] =
new Rules(extensions).root.parse(toml) match {
case Success(v, _) => Embed.root(v)
case f: Failure[_, _] => Left(List() -> f.msg)
}
Expand All @@ -24,23 +24,37 @@ object Toml {
codec(table, d, 0).right.map(generic.from)
}

def apply[D <: HList, R <: HList](toml: String)(implicit
def apply[D <: HList, R <: HList](
toml : String,
extensions: Set[Extension]
)(implicit
generic : LabelledGeneric.Aux[A, R],
defaults : Default.AsRecord.Aux[A, D],
defaultMapper: util.RecordToMap[D],
codec : Codec[R]
): Either[Parse.Error, A] = {
val d = defaultMapper(defaults())
parse(toml).right.flatMap(codec(_, d, 0).right.map(generic.from))
parse(toml, extensions)
.right
.flatMap(codec(_, d, 0).right.map(generic.from))
}

def apply[D <: HList, R <: HList](toml: String)(
implicit
generic : LabelledGeneric.Aux[A, R],
defaults : Default.AsRecord.Aux[A, D],
defaultMapper: util.RecordToMap[D],
codec : Codec[R]
): Either[Parse.Error, A] = apply(toml, Set())
}

class CodecHelperValue[A] {
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[Parse.Error, A] =
parse(toml).right.flatMap(codec(_, Map(), 0))
def apply(toml: String, extensions: Set[Extension] = Set())
(implicit codec: Codec[A]): Either[Parse.Error, A] =
parse(toml, extensions).right.flatMap(codec(_, Map(), 0))
}

def parseAs [T]: CodecHelperGeneric[T] = new CodecHelperGeneric[T]
Expand Down
2 changes: 1 addition & 1 deletion shared/src/test/scala/toml/CodecSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class CodecSpec extends FunSuite {
case class Pair(a: Int)

val pair = """a = 1"""
assert(Toml.parseAs[Pair](pair) == Right(Pair(1)))
assert(Toml.parseAs[Pair](pair, Set()) == Right(Pair(1)))

case class Pairs(a: Int, b: Int)
val pairs =
Expand Down
10 changes: 5 additions & 5 deletions shared/src/test/scala/toml/GeneratedSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ class GeneratedSpec extends PropSpec with PropertyChecks with Matchers {
}
}

property("Parse pairs (with `node` parser)") {
property("Parse pairs (with `root` parser)") {
import Generators.Tables._
forAll(pairGen) { s: String =>
shouldBeSuccess(Rules.node.parse(s))
shouldBeSuccess(Rules.root.parse(s))
}
}

Expand All @@ -80,14 +80,14 @@ class GeneratedSpec extends PropSpec with PropertyChecks with Matchers {
property("Parse tables") {
import Generators.Tables._
forAll(tableGen) { s: String =>
shouldBeSuccess[Node.NamedTable](Rules.table.parse(s))
shouldBeSuccess[Node.NamedTable]((Rules.skip ~ Rules.table).parse(s))
}
}

property("Parse tables (with `node` parser)") {
property("Parse tables (with `root` parser)") {
import Generators.Tables._
forAll(tableGen) { s: String =>
shouldBeSuccess(Rules.node.parse(s))
shouldBeSuccess(Rules.root.parse(s))
}
}
}
31 changes: 31 additions & 0 deletions shared/src/test/scala/toml/ParseSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,27 @@ import Node._
import org.scalatest.FunSuite

class ParseSpec extends FunSuite {
test("Parse strings") {
val toml =
"""
|lines = '''
|
|The first newline is
|trimmed in raw strings.
| All other whitespace
| is preserved.
|'''
|""".stripMargin

val result = Toml.parse(toml)
assert(result == Right(Tbl(Map("lines" -> Value.Str(
"\n" +
"The first newline is\n" +
"trimmed in raw strings.\n" +
" All other whitespace\n" +
" is preserved.\n")))))
}

test("Redefine value on root level") {
val toml =
"""a = 23
Expand Down Expand Up @@ -34,4 +55,14 @@ class ParseSpec extends FunSuite {
val result = Toml.parse(toml)
assert(result == Left((List("a", "b"), "Cannot redefine value")))
}

test("Extension: Parse inline tables with trailing comma") {
val result = Toml.parse("""key = {
a = 23,
b = 42,
}""", Set(Extension.MultiLineInlineTables))

assert(result == Right(Tbl(
Map("key" -> Tbl(Map("a" -> Num(23), "b" -> Num(42)))))))
}
}
40 changes: 38 additions & 2 deletions shared/src/test/scala/toml/RulesSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,51 @@ class RulesSpec extends FunSuite with Matchers {
testSuccess(example)
}

test("Parse inline tables") {
test("Parse inline tables (1)") {
val example =
"""
|a = { name = "value", name2 = "value2" }
|b = { name = "value" }
""".stripMargin

testSuccess(example)
}

test("Extension: Parse inline tables with new line") {
// See https://github.com/toml-lang/toml/issues/516
val example =
"""
|a = { name = "value", name2 = "value2" }
|b = { name = "value"
| }
""".stripMargin

testFailure(example)
testSuccess(example, new Rules(Set(Extension.MultiLineInlineTables)))
}

test("Extension: Parse inline tables with trailing comma") {
val example =
"""
|a = { name = "value", name2 = "value2" }
|b = { name = "value", }
""".stripMargin

testFailure(example)
testSuccess(example, new Rules(Set(Extension.MultiLineInlineTables)))
}

test("Extension: Parse inline tables with trailing comma and comment") {
val example =
"""
|a = { name = "value", name2 = "value2" }
|b = { name = "value",
| # Trailing comma
| }
""".stripMargin
testSuccess(example)

testFailure(example)
testSuccess(example, new Rules(Set(Extension.MultiLineInlineTables)))
}

test("Parse complex table keys") {
Expand Down
10 changes: 5 additions & 5 deletions shared/src/test/scala/toml/TestHelpers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import org.scalatest.Matchers
object TestHelpers {
import Matchers._

def testSuccess(example: String): Root =
Rules.root.parse(example) match {
def testSuccess(example: String, rules: Rules = Rules): Root =
rules.root.parse(example) match {
case Success(v, _) => v
case f: Failure[_, _] => fail(s"Failed to parse `$example`: ${f.msg}")
}

def testFailure(example: String): Unit =
Rules.root.parse(example) match {
def testFailure(example: String, rules: Rules = Rules): Unit =
rules.root.parse(example) match {
case Success(_, _) => fail(s"Did not fail: $example")
case _: Failure[_, _] =>
}
Expand All @@ -29,4 +29,4 @@ object TestHelpers {
case s: Success[T, _, _] => fail(s"$r is not a Failure.")
case f: Failure[_, _] =>
}
}
}

0 comments on commit d0d988d

Please sign in to comment.