Skip to content

Commit

Permalink
core: add date parser with testing
Browse files Browse the repository at this point in the history
- Parse RFC3339 dates as TOML requires

- Tests for date parser

- Update `fastparse` to 0.3.4

- Minor changes to compile after upgrade
  • Loading branch information
jvican committed Jan 5, 2016
1 parent 9e5c9e7 commit b1e88e3
Show file tree
Hide file tree
Showing 10 changed files with 141 additions and 53 deletions.
24 changes: 21 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,15 +1,33 @@
name := "TOML Parser in Scala"
name := "stoml"

organization := "com.github.jvican"

version := "1.0"
version := "1.1"

scalaVersion := "2.11.7"

libraryDependencies ++= Vector(
"com.lihaoyi" %% "fastparse" % "0.3.2",
"com.lihaoyi" %% "fastparse" % "0.3.4",
"org.scalacheck" %% "scalacheck" % "1.12.5" % "test",
"org.scalatest" % "scalatest_2.11" % "2.2.4" % "test"
)

licenses := Seq("MIT License" -> url("http://www.opensource.org/licenses/MIT"))

sonatypeProfileName := "com.github.jvican"

pomExtra in Global := {
<url>https://github.com/jvican/stoml.git</url>
<scm>
<developerConnection>scm:git:git@github.com:jvican</developerConnection>
<url>https://github.com/jvican/stoml.git</url>
<connection>scm:git:git@github.com:jvican/stoml.git</connection>
</scm>
<developers>
<developer>
<id>jvican</id>
<name>Jorge Vicente Cantero</name>
<url>https://github.com/jvican</url>
</developer>
</developers>
}
46 changes: 31 additions & 15 deletions src/main/scala/toml/TomlParser.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package toml

import scala.language.{implicitConversions, postfixOps}
import java.util.{Date => JDate}
import java.text.SimpleDateFormat

object Toml extends TomlSymbol {
case class NamedFunction[T, V](f: T => V, name: String)
Expand All @@ -9,30 +11,26 @@ object Toml extends TomlSymbol {
override def toString() = name
}

sealed trait Elem extends Any {
def v: Any
}

sealed trait Elem extends Any { def v: Any }
sealed trait Node extends Any with Elem

sealed trait Bool extends Elem
case object True extends Bool {def v = true}
case object False extends Bool {def v = false}

case class Str(v: String) extends AnyVal with Elem
object Str {
def dequoteStr(s: String, q: String) =
s.stripPrefix(q).stripSuffix(q)

def cleanStr(s: String): String =
dequoteStr(dequoteStr(s, SingleQuote), DoubleQuote)

def cleanedApply(s: String): Str = Str(cleanStr(s))
}

case class Str(v: String) extends AnyVal with Elem
case class Integer(v: Long) extends AnyVal with Elem
case class Real(v: Double) extends AnyVal with Elem
case class Arr(v: Seq[Elem]) extends AnyVal with Elem
case class Date(v: JDate) extends AnyVal with Elem
case class Pair(v: (String, Elem)) extends AnyVal with Node

type TableName = Vector[String]
Expand Down Expand Up @@ -74,6 +72,7 @@ trait TomlParser extends ParserUtil with TomlSymbol {
val WS: P0 = P { NoCut(NoTrace((WS0 | comment | newline).rep.?)) }

val letters = P { CharsWhile(Letters) }
val digit = P { CharIn('0' to '9') }
val digits = P { CharsWhile(Digits) }

val literalChars = NamedFunction(!SingleQuote.contains(_: Char), "LitStr")
Expand Down Expand Up @@ -103,13 +102,30 @@ trait TomlParser extends ParserUtil with TomlSymbol {
val `false` = P { "false" } map (_ => False)
val boolean: Parser[Bool] = P { `true` | `false` }

lazy val date: Parser[Date] =
rfc3339.opaque("<valid-date-rfc3339>").map { t =>
/* Even though this extra parsing is not necessary,
* it is done just for simplicity, avoiding the use
* of `java.util.Calendar` instances. */
Date(formatter.parse(t))
}

private val formatter =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")

def twice[T](p: Parser[T]) = p ~ p
def fourTimes[T](p: Parser[T]) = twice(p) ~ twice(p)
val rfc3339: Parser[String] =
P { fourTimes(digit) ~ "-" ~ twice(digit) ~ "-" ~
twice(digit) ~ "T" ~ twice(digit) ~ ":" ~
twice(digit) ~ ":" ~ twice(digit) ~ ("." ~
digit.rep(min=3, max=3)).? ~ "Z".? }.!

val dashes = P { CharIn(Dashes) }
val bareKey = P { (letters | digits | dashes).rep(min=1) }
val validKey = P { bareKey | NoCut(basicStr) }.!
val bareKey = P { (letters | digits | dashes).rep(min=1) }.!
val validKey: Parser[String] = P { bareKey | NoCut(basicStr) }.!
lazy val pair: Parser[Pair] =
P { validKey ~ WS0.? ~ "=" ~ WS0.? ~ elem } map {
kv: (String, Elem) => Pair(kv._1, kv._2)
}
P { validKey ~ WS0.? ~ "=" ~ WS0.? ~ elem } map Pair
lazy val array: Parser[Arr] =
P { "[" ~ WS ~ elem.rep(sep=WS0.? ~ "," ~/ WS) ~ WS ~ "]" } map Arr

Expand All @@ -123,7 +139,7 @@ trait TomlParser extends ParserUtil with TomlSymbol {
}

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

lazy val node: Parser[_ <: Node] = P { WS ~ (pair | table) ~ WS }
Expand All @@ -149,7 +165,7 @@ object TomlParserApi extends TomlParser {
}

import fastparse.all._
import fastparse.core.Result
def toToml(s: String): Result[TomlContent] =
import fastparse.core.Parsed
def toToml(s: String): Parsed[TomlContent] =
(nodes map TomlContent.apply).parse(s)
}
3 changes: 1 addition & 2 deletions src/test/scala/api/TomlParserApiSpec.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package api

import fastparse.core.Result.{Failure, Success}
import fastparse.core.Parsed.{Failure, Success}
import org.scalatest.{Matchers, FunSpec}

class TomlParserApiSpec extends FunSpec with Matchers {
Expand All @@ -17,7 +17,6 @@ class TomlParserApiSpec extends FunSpec with Matchers {
it("should parse a file correctly, parsing also the EOF") {
toToml(smallFileTest) match {
case Success(v, _) =>
println(v)
(v lookup Vector("num", "theory")) should not be empty
(v lookup Vector("best-author-ever")) should not be empty
case f: Failure =>
Expand Down
11 changes: 6 additions & 5 deletions src/test/scala/toml/ArrayTomlSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import org.scalatest.{Matchers, PropSpec}

import scala.language.postfixOps


trait ArrayTomlGen {
this: TomlSymbol
with StringTomlGen
Expand All @@ -30,10 +29,12 @@ trait ArrayTomlGen {
} yield arrayFormat(elems, (c1, ss, c2))
}

class ArrayTomlSpec extends PropSpec with PropertyChecks with Matchers
with NumbersTomlGen with StringTomlGen
with ArrayTomlGen with TomlParser
with TestParserUtil {
class ArrayTomlSpec extends PropSpec
with PropertyChecks with Matchers
with NumbersTomlGen with StringTomlGen
with ArrayTomlGen with TomlParser
with TestParserUtil {

property("parse arrays") {
forAll(arrayGen) {
s: String =>
Expand Down
10 changes: 6 additions & 4 deletions src/test/scala/toml/BooleanTomlSpec.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package toml

import fastparse.core.Result.Success
import fastparse.core.Parsed.Success
import org.scalacheck.Gen
import org.scalatest.prop._
import org.scalatest.{Matchers, PropSpec}
Expand All @@ -21,9 +21,11 @@ trait BooleanTomlGen {
Gen.oneOf("True", "False")
}

class BooleanTomlSpec extends PropSpec with PropertyChecks with Matchers
with BooleanTomlGen with TomlParser
with TestParserUtil {
class BooleanTomlSpec extends PropSpec
with PropertyChecks with Matchers
with BooleanTomlGen with TomlParser
with TestParserUtil {

property("parse boolean literals") {
forAll(validBoolGen) {
s: String =>
Expand Down
48 changes: 48 additions & 0 deletions src/test/scala/toml/DateTomlSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package toml

import org.scalacheck.Gen
import org.scalatest.prop._
import org.scalatest.{Matchers, PropSpec}
import fastparse.core.Parsed.Success

trait DateTomlGen {
import Gen.chooseNum

def pad(n: Int, s: String): String =
if(s.length < n) ("0" * (n - s.length)) + s else s
def genNum(digits: Int, from: Int, to: Int): Gen[String] =
chooseNum(from, to).map(n => pad(digits, n.toString))

/* This check is not covering all the formats */
val dateFormatGen: Gen[String] = for {
day <- genNum(2, 0, 28)
month <- genNum(2, 1, 12)
year <- genNum(4, 0, 2200)
hour <- genNum(2, 0, 23)
minute <- genNum(2, 0, 59)
second <- genNum(2, 0, 59)
micro <- genNum(3, 0, 999)
} yield (year + "-" + month + "-" + day +
"T" + hour + ":" + minute + ":" +
second + "." + micro + "Z")
}

class DateTomlSpec extends PropSpec
with PropertyChecks with Matchers
with DateTomlGen with TomlParser
with TestParserUtil {

property("parse dates following the RFC 3339 spec (`date` parser)") {
forAll(dateFormatGen) {
s =>
shouldBeSuccess(date.parse(s))
}
}

property("parse dates following the RFC 3339 spec") {
forAll(dateFormatGen) {
s =>
shouldBeSuccess(elem.parse(s))
}
}
}
10 changes: 6 additions & 4 deletions src/test/scala/toml/NumbersTomlSpec.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package toml

import fastparse.core.Result.Success
import fastparse.core.Parsed.Success
import org.scalacheck.Gen
import org.scalatest.prop._
import org.scalatest.{Matchers, PropSpec}
Expand All @@ -27,9 +27,11 @@ trait NumbersTomlGen {
} yield fs
}

class NumbersTomlSpec extends PropSpec with PropertyChecks with Matchers
with NumbersTomlGen with TomlParser
with TestParserUtil {
class NumbersTomlSpec extends PropSpec
with PropertyChecks with Matchers
with NumbersTomlGen with TomlParser
with TestParserUtil {

import Toml._
property("parse integers") {
forAll(validLongGen) {
Expand Down
20 changes: 10 additions & 10 deletions src/test/scala/toml/StringTomlSpec.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package toml

import fastparse.core.Result.Success
import fastparse.core.Parsed.Success
import org.scalacheck.Gen
import org.scalatest.prop._
import org.scalatest.{Matchers, PropSpec}
Expand All @@ -11,7 +11,6 @@ trait StringTomlGen {
def enquoteStr(s: String, q: String): String =
q + s + q


def quotedStrGen(quote: String): Gen[String] = for {
s <- Gen.alphaStr
if s != ""
Expand All @@ -25,17 +24,18 @@ trait StringTomlGen {
def invalidStrGen: Gen[String] = for {
s <- Gen.alphaStr
f <- Gen.oneOf(List(
SingleQuote + s,
s + SingleQuote,
DoubleQuote + s,
s + DoubleQuote
))
SingleQuote + s,
s + SingleQuote,
DoubleQuote + s,
s + DoubleQuote))
} yield f
}

class StringTomlSpec extends PropSpec with PropertyChecks with Matchers
with StringTomlGen with TomlParser
with TestParserUtil {
class StringTomlSpec extends PropSpec
with PropertyChecks with Matchers
with StringTomlGen with TomlParser
with TestParserUtil {

import Toml._
property("parse single and double-quoted strings") {
forAll(validStrGen) {
Expand Down
12 changes: 7 additions & 5 deletions src/test/scala/toml/TableTomlSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,13 @@ trait TableTomlGen {

}

class TableTomlSpec extends PropSpec with PropertyChecks with Matchers
with BooleanTomlGen with StringTomlGen
with NumbersTomlGen with TableTomlGen
with CommentTomlGen with TomlParser
with TestParserUtil {
class TableTomlSpec extends PropSpec
with PropertyChecks with Matchers
with BooleanTomlGen with StringTomlGen
with NumbersTomlGen with TableTomlGen
with CommentTomlGen with TomlParser
with TestParserUtil {

import Toml._

property("parse pairs (key and value)") {
Expand Down
10 changes: 5 additions & 5 deletions src/test/scala/toml/TestParserUtil.scala
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
package toml

import fastparse.core.Result
import fastparse.core.Result.{Failure, Success}
import fastparse.core.Parsed
import fastparse.core.Parsed.{Failure, Success}
import org.scalatest.Matchers

trait TestParserUtil {
this: Matchers =>

def shouldBeSuccess[T](r: Result[T]) = r match {
def shouldBeSuccess[T](r: Parsed[T]) = r match {
case s: Success[T] => assert(true)
case f: Failure => fail(s"$r is not a Success.")
}

def shouldBeFailure[T](r: Result[T]) = r match {
def shouldBeFailure[T](r: Parsed[T]) = r match {
case s: Success[T] => fail(s"$r is not a Failure.")
case f: Failure => assert(true)
}
}
}

0 comments on commit b1e88e3

Please sign in to comment.