Skip to content
This repository was archived by the owner on Oct 24, 2024. It is now read-only.

Commit d0d988d

Browse files
authored
Merge pull request #15 from sparsetech/feat/language-extensions
Introduce language extension for multi-line inline tables
2 parents bef7ade + 10a891e commit d0d988d

File tree

9 files changed

+150
-52
lines changed

9 files changed

+150
-52
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,20 @@ scalaDeps = [
133133
]
134134
```
135135

136+
#### Language Extensions
137+
toml-scala supports the following language extensions which are disabled by default:
138+
139+
* [New lines and trailing commas in inline tables](https://github.com/toml-lang/toml/issues/516)
140+
141+
To enable them, pass in a set of extensions to the `parse()` or `parseAs()` function as a second argument:
142+
143+
```scala
144+
toml.Toml.parse("""key = {
145+
a = 23,
146+
b = 42,
147+
}""", Set(toml.Extension.MultiLineInlineTables))
148+
```
149+
136150
## Links
137151
* [ScalaDoc](https://www.javadoc.io/doc/tech.sparse/toml-scala_2.12/)
138152

jvm/src/main/scala/toml/PlatformRules.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import java.time._
44

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

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

@@ -39,4 +39,4 @@ trait PlatformRules { this: Rules.type =>
3939
}
4040

4141
val date = P(offsetDateTime | localDateTime | localDate | localTime)
42-
}
42+
}

shared/src/main/scala/toml/Rules.scala

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,31 @@ private[toml] case class NamedFunction[T, V](f: T => V, name: String)
99
override def toString: String = name
1010
}
1111

12-
object Rules extends PlatformRules {
12+
sealed trait Extension
13+
object Extension {
14+
case object MultiLineInlineTables extends Extension
15+
}
16+
17+
case object Rules extends toml.Rules(Set())
18+
19+
class Rules(extensions: Set[Extension]) extends PlatformRules {
1320
import Constants._
21+
import Extension._
1422

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

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

21-
val whitespace = P(CharIn(WhitespaceChars.toList))
22-
val whitespaces = P(whitespace.rep(1))
30+
val skip = P(NoCut(NoTrace((whitespace | comment | newLine).rep)))
31+
val skipWs = P(NoCut(NoTrace(whitespace.rep)))
2332

24-
val skip = P(NoCut(NoTrace((whitespaces | comment | newLine).rep)))
25-
26-
val letter = P(CharIn(LettersRange))
27-
val letters = P(letter.rep(1))
28-
val digit = P(CharIn(NumbersRange))
29-
val digits = P(digit.rep(1))
30-
31-
val skipSpaces = P(CharsWhile(_.isWhitespace).?)
33+
val letter = P(CharIn(LettersRange))
34+
val digit = P(CharIn(NumbersRange))
35+
val digits = P(digit.rep(1))
36+
val dash = P(CharIn(Dashes.toList))
3237

3338
val StringChars = NamedFunction(!"\"\\".contains(_: Char), "StringChars")
3439
val strChars = P(CharsWhile(StringChars))
@@ -52,14 +57,14 @@ object Rules extends PlatformRules {
5257
val multiLineBasicStr: Parser[Value.Str] =
5358
P(
5459
MultiLineDoubleQuote ~/
55-
skipSpaces ~
60+
newLine.? ~
5661
(!MultiLineDoubleQuote ~ AnyChar).rep.! ~
5762
MultiLineDoubleQuote
5863
).map(str => Value.Str(Unescape.unescapeJavaString(str)))
5964
val multiLineLiteralStr: Parser[Value.Str] =
6065
P(
6166
MultiLineSingleQuote ~/
62-
skipSpaces ~
67+
newLine.? ~
6368
(!MultiLineSingleQuote ~ AnyChar).rep.! ~
6469
MultiLineSingleQuote
6570
).map(Value.Str)
@@ -98,44 +103,42 @@ object Rules extends PlatformRules {
98103
val `false` = P("false").map(_ => Value.Bool(false))
99104
val boolean = P(`true` | `false`)
100105

101-
val dashes = P(CharIn(Dashes.toList))
102-
val bareKey = P((letters | digits | dashes).rep(min = 1)).!
106+
val bareKey = P((letter | digit | dash).rep(min = 1)).!
103107
val validKey: Parser[String] =
104108
P(NoCut(basicStr.map(_.value)) | NoCut(literalStr.map(_.value)) | bareKey)
105109
val pair: Parser[(String, Value)] =
106-
P(validKey ~ whitespaces.? ~ "=" ~ whitespaces.? ~ elem)
110+
P(validKey ~ skipWs ~ "=" ~ skipWs ~ elem)
107111
val array: Parser[Value.Arr] =
108112
P("[" ~ skip ~ elem.rep(sep = "," ~ skip) ~ ",".? ~ skip ~ "]")
109113
.map(l => Value.Arr(l.toList))
110114
val inlineTable: Parser[Value.Tbl] =
111-
P("{" ~ skip ~ pair.rep(sep = "," ~ skip) ~ ",".? ~ skip ~ "}")
112-
.map(p => Value.Tbl(p.toMap))
115+
(if (extensions.contains(MultiLineInlineTables))
116+
P("{" ~ skip ~ pair.rep(sep = "," ~ skip) ~ ",".? ~ skip ~ "}")
117+
else
118+
P("{" ~ skipWs ~ pair.rep(sep = "," ~ skipWs) ~ skipWs ~ "}")
119+
).map(p => Value.Tbl(p.toMap))
113120

114121
val tableIds: Parser[Seq[String]] =
115-
P(validKey.rep(min = 1, sep = whitespaces.? ~ "." ~ whitespaces.?).map(_.toSeq))
122+
P(validKey.rep(min = 1, sep = skipWs ~ "." ~ skipWs).map(_.toSeq))
116123
val tableDef: Parser[Seq[String]] =
117-
P("[" ~ whitespaces.? ~ tableIds ~ whitespaces.? ~ "]")
124+
P("[" ~ skipWs ~ tableIds ~ skipWs ~ "]")
118125
val tableArrayDef: Parser[Seq[String]] =
119-
P("[[" ~ whitespaces.? ~ tableIds ~ whitespaces.? ~ "]]")
126+
P("[[" ~ skipWs ~ tableIds ~ skipWs ~ "]]")
120127

121128
val pairNode: Parser[Node.Pair] = pair.map { case (k, v) => Node.Pair(k, v) }
122129
val table: Parser[Node.NamedTable] =
123-
P(skip ~ tableDef ~ skip ~ pair.rep(sep = skip)).map { case (a, b) =>
130+
P(tableDef ~ skip ~ pair.rep(sep = skip)).map { case (a, b) =>
124131
Node.NamedTable(a.toList, b.toList)
125132
}
126133
val tableArray: Parser[Node.NamedArray] =
127-
P(skip ~ tableArrayDef ~ skip ~ pair.rep(sep = skip)).map { case (a, b) =>
134+
P(tableArrayDef ~ skip ~ pair.rep(sep = skip)).map { case (a, b) =>
128135
Node.NamedArray(a.toList, b.toList)
129136
}
130137

131-
lazy val elem: Parser[Value] = P {
132-
skip ~
133-
(date | string | boolean | double | integer | array | inlineTable) ~
134-
skip
135-
}
136-
137-
lazy val node: Parser[Node] = P(skip ~ (pairNode | table | tableArray))
138+
lazy val elem: Parser[Value] =
139+
P(date | string | boolean | double | integer | array | inlineTable)
138140

139-
val root: Parser[Root] = P(node.rep(sep = skip) ~ skip ~ End)
141+
val node: Parser[Node] = P(pairNode | table | tableArray)
142+
val root: Parser[Root] = P(skip ~ node.rep(sep = skip) ~ skip ~ End)
140143
.map(nodes => Root(nodes.toList))
141144
}

shared/src/main/scala/toml/Toml.scala

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import shapeless._
55
import scala.meta.internal.fastparse.core.Parsed._
66

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

27-
def apply[D <: HList, R <: HList](toml: String)(implicit
27+
def apply[D <: HList, R <: HList](
28+
toml : String,
29+
extensions: Set[Extension]
30+
)(implicit
2831
generic : LabelledGeneric.Aux[A, R],
2932
defaults : Default.AsRecord.Aux[A, D],
3033
defaultMapper: util.RecordToMap[D],
3134
codec : Codec[R]
3235
): Either[Parse.Error, A] = {
3336
val d = defaultMapper(defaults())
34-
parse(toml).right.flatMap(codec(_, d, 0).right.map(generic.from))
37+
parse(toml, extensions)
38+
.right
39+
.flatMap(codec(_, d, 0).right.map(generic.from))
3540
}
41+
42+
def apply[D <: HList, R <: HList](toml: String)(
43+
implicit
44+
generic : LabelledGeneric.Aux[A, R],
45+
defaults : Default.AsRecord.Aux[A, D],
46+
defaultMapper: util.RecordToMap[D],
47+
codec : Codec[R]
48+
): Either[Parse.Error, A] = apply(toml, Set())
3649
}
3750

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

42-
def apply(toml: String)(implicit codec: Codec[A]): Either[Parse.Error, A] =
43-
parse(toml).right.flatMap(codec(_, Map(), 0))
55+
def apply(toml: String, extensions: Set[Extension] = Set())
56+
(implicit codec: Codec[A]): Either[Parse.Error, A] =
57+
parse(toml, extensions).right.flatMap(codec(_, Map(), 0))
4458
}
4559

4660
def parseAs [T]: CodecHelperGeneric[T] = new CodecHelperGeneric[T]

shared/src/test/scala/toml/CodecSpec.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class CodecSpec extends FunSuite {
6262
case class Pair(a: Int)
6363

6464
val pair = """a = 1"""
65-
assert(Toml.parseAs[Pair](pair) == Right(Pair(1)))
65+
assert(Toml.parseAs[Pair](pair, Set()) == Right(Pair(1)))
6666

6767
case class Pairs(a: Int, b: Int)
6868
val pairs =

shared/src/test/scala/toml/GeneratedSpec.scala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@ class GeneratedSpec extends PropSpec with PropertyChecks with Matchers {
6363
}
6464
}
6565

66-
property("Parse pairs (with `node` parser)") {
66+
property("Parse pairs (with `root` parser)") {
6767
import Generators.Tables._
6868
forAll(pairGen) { s: String =>
69-
shouldBeSuccess(Rules.node.parse(s))
69+
shouldBeSuccess(Rules.root.parse(s))
7070
}
7171
}
7272

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

87-
property("Parse tables (with `node` parser)") {
87+
property("Parse tables (with `root` parser)") {
8888
import Generators.Tables._
8989
forAll(tableGen) { s: String =>
90-
shouldBeSuccess(Rules.node.parse(s))
90+
shouldBeSuccess(Rules.root.parse(s))
9191
}
9292
}
9393
}

shared/src/test/scala/toml/ParseSpec.scala

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,27 @@ import Node._
66
import org.scalatest.FunSuite
77

88
class ParseSpec extends FunSuite {
9+
test("Parse strings") {
10+
val toml =
11+
"""
12+
|lines = '''
13+
|
14+
|The first newline is
15+
|trimmed in raw strings.
16+
| All other whitespace
17+
| is preserved.
18+
|'''
19+
|""".stripMargin
20+
21+
val result = Toml.parse(toml)
22+
assert(result == Right(Tbl(Map("lines" -> Value.Str(
23+
"\n" +
24+
"The first newline is\n" +
25+
"trimmed in raw strings.\n" +
26+
" All other whitespace\n" +
27+
" is preserved.\n")))))
28+
}
29+
930
test("Redefine value on root level") {
1031
val toml =
1132
"""a = 23
@@ -34,4 +55,14 @@ class ParseSpec extends FunSuite {
3455
val result = Toml.parse(toml)
3556
assert(result == Left((List("a", "b"), "Cannot redefine value")))
3657
}
58+
59+
test("Extension: Parse inline tables with trailing comma") {
60+
val result = Toml.parse("""key = {
61+
a = 23,
62+
b = 42,
63+
}""", Set(Extension.MultiLineInlineTables))
64+
65+
assert(result == Right(Tbl(
66+
Map("key" -> Tbl(Map("a" -> Num(23), "b" -> Num(42)))))))
67+
}
3768
}

shared/src/test/scala/toml/RulesSpec.scala

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,51 @@ class RulesSpec extends FunSuite with Matchers {
6363
testSuccess(example)
6464
}
6565

66-
test("Parse inline tables") {
66+
test("Parse inline tables (1)") {
67+
val example =
68+
"""
69+
|a = { name = "value", name2 = "value2" }
70+
|b = { name = "value" }
71+
""".stripMargin
72+
73+
testSuccess(example)
74+
}
75+
76+
test("Extension: Parse inline tables with new line") {
77+
// See https://github.com/toml-lang/toml/issues/516
78+
val example =
79+
"""
80+
|a = { name = "value", name2 = "value2" }
81+
|b = { name = "value"
82+
| }
83+
""".stripMargin
84+
85+
testFailure(example)
86+
testSuccess(example, new Rules(Set(Extension.MultiLineInlineTables)))
87+
}
88+
89+
test("Extension: Parse inline tables with trailing comma") {
90+
val example =
91+
"""
92+
|a = { name = "value", name2 = "value2" }
93+
|b = { name = "value", }
94+
""".stripMargin
95+
96+
testFailure(example)
97+
testSuccess(example, new Rules(Set(Extension.MultiLineInlineTables)))
98+
}
99+
100+
test("Extension: Parse inline tables with trailing comma and comment") {
67101
val example =
68102
"""
69103
|a = { name = "value", name2 = "value2" }
70104
|b = { name = "value",
71105
| # Trailing comma
72106
| }
73107
""".stripMargin
74-
testSuccess(example)
108+
109+
testFailure(example)
110+
testSuccess(example, new Rules(Set(Extension.MultiLineInlineTables)))
75111
}
76112

77113
test("Parse complex table keys") {

shared/src/test/scala/toml/TestHelpers.scala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ import org.scalatest.Matchers
88
object TestHelpers {
99
import Matchers._
1010

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

17-
def testFailure(example: String): Unit =
18-
Rules.root.parse(example) match {
17+
def testFailure(example: String, rules: Rules = Rules): Unit =
18+
rules.root.parse(example) match {
1919
case Success(_, _) => fail(s"Did not fail: $example")
2020
case _: Failure[_, _] =>
2121
}
@@ -29,4 +29,4 @@ object TestHelpers {
2929
case s: Success[T, _, _] => fail(s"$r is not a Failure.")
3030
case f: Failure[_, _] =>
3131
}
32-
}
32+
}

0 commit comments

Comments
 (0)