Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fastparse implementation #1

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,5 @@ resolvers ++= Seq(
libraryDependencies ++= Seq(
"com.chuusai" %%% "shapeless" % "2.3.3"
)

libraryDependencies += "com.lihaoyi" %%% "fastparse" % "1.0.0"
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.github.aborg0.caseyclassy

import fastparse.all._
import java.time.{LocalDate, LocalTime}

import shapeless._

import scala.reflect.runtime.universe._

private[caseyclassy] trait FPGenericImplementations {
implicit def booleanParse: FastParseParse[Boolean] = () => P("true" | "false").!.map(_.toBoolean)

private[this] def integral: Parser[String] = P("-".? ~ CharIn('0' to '9').rep(1)).!

private[this] def numeric: Parser[String] = P("NaN" | "-".? ~ "Infinity" |
("-".? ~ CharIn('0' to '9').rep(1) ~
("." ~/ CharIn('0' to '9').rep(1)).? ~
(CharIn("eE") ~/ CharIn("+-").? ~/ CharIn('0' to '9').rep(1)).?)).!

implicit def longParse: FastParseParse[Long] = () => integral.map(_.toLong)

implicit def intParse: FastParseParse[Int] = () => integral.map(_.toInt)

implicit def shortParse: FastParseParse[Short] = () => integral.map(_.toShort)

implicit def byteParse: FastParseParse[Byte] = () => integral.map(_.toByte)

implicit def doubleParse: FastParseParse[Double] = () => numeric.!.map(_.toDouble)

implicit def floatParse: FastParseParse[Float] = () => numeric.!.map(_.toFloat)

implicit def parseHNil: FastParseParse[HNil] = () => Pass.map(_ => HNil)

implicit def parseCNil: FastParseParse[CNil] = () => Fail.log("CNil")

implicit def parseProduct[Head, Tail <: HList](implicit headParse: Lazy[FastParseParse[Head]],
tailParse: Lazy[FastParseParse[Tail]]): FastParseParse[Head :: Tail] =
() => (headParse.value.parser() ~ ",".? ~ tailParse.value.parser()).map { case (h, t) => h :: t }

implicit def parseCoproduct[Head, Tail <: Coproduct](implicit headParse: Lazy[FastParseParse[Head]],
tailParse: Lazy[FastParseParse[Tail]]): FastParseParse[Head :+: Tail] =
() => headParse.value.parser().map(Inl(_)) | tailParse.value.parser().map(Inr(_))

implicit def generic[A: TypeTag, R](implicit gen: Generic.Aux[A, R], argParse: FastParseParse[R]): FastParseParse[A] = () => {
val typeKind = implicitly[TypeTag[A]]
if (typeKind.tpe.typeSymbol.isAbstract /* Approximation of sealed trait */ ) argParse.parser().map(gen.from) else {
val name = typeKind.tpe.typeSymbol.name.toString
if (name.startsWith("Tuple"))
P("(" ~/ argParse.parser() ~ ")").map(gen.from)
else
P(name ~ (("(" ~/ argParse.parser() ~ ")") | argParse.parser())).map(gen.from)
}
}
}

case object FastParseParseCaseClass extends ParseCaseClass with FPGenericImplementations {

override def to[A](input: String)(implicit parse: Lazy[Parse[A]]): A = {
parse.value.parse(input)
}

def apply[A](implicit p: Lazy[FastParseParse[A]]): FastParseParse[A] = p.value

//region Custom overrides of special types
implicit def stringParse: FastParseParse[String] = () =>
P(CharsWhile(c => c != ',' && c != ')').!) | P("").!

implicit def timeParse: FastParseParse[LocalTime] = () => P(
CharIn('0' to '9').rep(2, "", 2) ~ ":" ~
CharIn('0' to '9').rep(2, "", 2) ~
(":" ~/ CharIn('0' to '9').rep(2, "", 2) ~
("." ~/ CharIn('0' to '9').rep(1)).?).?).!.map(LocalTime.parse(_))

implicit def dateParse: FastParseParse[LocalDate] = () =>
P(CharIn('0' to '9').rep(4, "", 4) ~ "-" ~
CharIn('0' to '9').rep(1, "", 2) ~ "-" ~
CharIn('0' to '9').rep(1, "", 2)).!.map(LocalDate.parse(_))

implicit def seqConverter[A](implicit parseA: FastParseParse[A]): FastParseParse[Seq[A]] = () =>
P(("WrappedArray" | "List" | "Vector") ~ "(" ~/ parseA.parser().rep(sep = ", ") ~ ")")
//endregion
}

trait FastParseParse[A] extends Parse[A] {
protected[caseyclassy] def parser(): Parser[A]

override def parse(input: String): A = parser().parse(input).fold((p, i, e) =>
throw new IllegalArgumentException(s"Expected: $p at position: $i"), (a, i) => a)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.github.aborg0.caseyclassy
import shapeless.Lazy

trait ParseCaseClass {
def to[A <: AnyRef](input: String)(implicit parse: Lazy[Parse[A]]): A
def to[A /*<: AnyRef*/](input: String)(implicit parse: Lazy[Parse[A]]): A
}

trait Parse[A] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ private[caseyclassy] trait GenericImplementations {

case object RegexParseCaseClass extends ParseCaseClass with GenericImplementations {

override def to[A <: AnyRef](input: String)(implicit parse: Lazy[Parse[A]]): A = {
override def to[A/* <: AnyRef*/](input: String)(implicit parse: Lazy[Parse[A]]): A = {
parse.value.parse(input)
}

Expand Down
95 changes: 92 additions & 3 deletions src/main/tut/Examples.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# Examples

## Intro
There might be multiple implementations of ParseCaseClass, currently only RegexParseCaseClass is supported:
There might be multiple implementations of `ParseCaseClass`, currently only `RegexParseCaseClass` and `FastParseParseCaseClass` are supported, but only one should be used in a scope (in this case `RegexParseCaseClass`):
```tut:silent
import com.github.aborg0.caseyclassy.RegexParseCaseClass
import com.github.aborg0.caseyclassy.RegexParseCaseClass._
import java.time.{LocalDate, LocalTime}
import shapeless._
```
It might possible to parse simple values, like `Boolean`, `Byte`, `Short`, `Int`, `Long`, `Float`, `Double`, `LocalTime`, `LocalDate` and `String`s without comma (`,`) or closing parenthesis (`)`), but this library was designed to parse toString of algebraic data types (products -case classes, tuples- and coproducts) of them. Also some `Seq`s (`List`, `Vector`, `WrappedArray` (for varargs)) are supported.
It is possible to parse simple values, like `Boolean`, `Byte`, `Short`, `Int`, `Long`, `Float`, `Double`, `LocalTime`, `LocalDate` and `String`s without comma (`,`) or closing parenthesis (`)`), but this library was designed to parse toString of algebraic data types (products -case classes, tuples- and coproducts) of them. Also some `Seq`s (`List`, `Vector`, `WrappedArray` (for varargs)) are supported.

### Simple primitives

Expand Down Expand Up @@ -92,4 +92,93 @@ RegexParseCaseClass.to[Option[Either[String, Seq[Boolean]]]]("Some(Right(List(fa
RegexParseCaseClass.to[Option[Either[String, Seq[Boolean]]]]("Some(Left(List(false, true)))")
```

Please note that the `String` part contains only till the first `,` (`List(false`) within and no error is reported currently.
Please note that the `String` part contains only till the first `,` (`List(false`) within and no error is reported currently.

# Same content with FastParse (`FastParseParseCaseClass`)

```tut:silent:reset
```

## Intro
There might be multiple implementations of `ParseCaseClass`, currently only `RegexParseCaseClass` and `FastParseParseCaseClass` are supported, but only one should be used in a scope (in this case `FastParseParseCaseClass`):
```tut:silent
import com.github.aborg0.caseyclassy.FastParseParseCaseClass
import com.github.aborg0.caseyclassy.FastParseParseCaseClass._
import java.time.{LocalDate, LocalTime}
import shapeless._
```
It is possible to parse simple values, like `Boolean`, `Byte`, `Short`, `Int`, `Long`, `Float`, `Double`, `LocalTime`, `LocalDate` and `String`s without comma (`,`) or closing parenthesis (`)`), but this library was designed to parse toString of algebraic data types (products -case classes, tuples- and coproducts) of them. Also some `Seq`s (`List`, `Vector`, `WrappedArray` (for varargs)) are supported.

### Simple primitives

`LocalDate`:

```tut
val date: LocalDate = FastParseParseCaseClass.to[LocalDate]("2018-04-01")
```

or it is also possible to create a parser and reuse it:

```tut
val dateParser = FastParseParseCaseClass[LocalDate]
dateParser.parse("2018-04-01")
dateParser.parse("2018-04-22")
```

Tuple2 of `String` and `Int`:

```tut
FastParseParseCaseClass.to[(String, Int)]("( hello,4)")
```

Or in the other order:

```tut
val (i, s) = FastParseParseCaseClass.to[(Int, String)]("(4, hello)")
```

The error messages are not very good:

```tut:fail
val dateTuple1 = FastParseParseCaseClass.to[Tuple1[LocalDate]]("2018-04-01")
```

## Algebraic data types

With help of shapeless the following constructs are supported:
- case classes
- case objects
- sealed hierarchies
- tuples
- a few `Seq` types

### Case classes

```tut
case class Example(a: Int, s: String)
FastParseParseCaseClass.to[Example]("Example(-3, Hello)")
```

```tut
case object Dot

FastParseParseCaseClass.to[Dot.type]("Dot")
```

### Sealed hierarchies

#### Either

```tut
FastParseParseCaseClass.to[Either[Short, Boolean]]("Left(-1111)")
FastParseParseCaseClass.to[Either[Short, Boolean]]("Right(false)")
```

#### Option

```tut
FastParseParseCaseClass.to[Option[Option[Int]]]("Some(None)")
FastParseParseCaseClass.to[Option[Option[Int]]]("None")
FastParseParseCaseClass.to[Option[Either[String, Seq[Boolean]]]]("Some(Right(List()))")
FastParseParseCaseClass.to[Option[Either[String, Seq[Boolean]]]]("Some(Right(List(false, true)))")
```
88 changes: 88 additions & 0 deletions src/test/scala/com/github/aborg0/caseyclassy/FPSimpleTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.github.aborg0.caseyclassy

import java.time.LocalDate

import com.github.aborg0.caseyclassy.example.{SimpleBoolean, SimpleDouble, SimpleInt, SimpleObject}
import org.scalatest.FlatSpec
import org.scalatest.prop.{TableDrivenPropertyChecks, TableFor1, TableFor2}

class FPSimpleTests extends FlatSpec with TableDrivenPropertyChecks {
val implementations: TableFor1[ParseCaseClass] = Table("implementation", FastParseParseCaseClass)

behavior of "FastParseParseCaseClass for simple cases"
import FastParseParseCaseClass._

it should "parse SimpleDouble" in {
val simpleDoubleInputs: TableFor2[ParseCaseClass, SimpleDouble] = Table(
("implementation", "SimpleDouble"),
Seq(1d,
0d,
.25,
-2.5,
-0d,
7e-17,
Double.MaxValue,
Double.NaN,
Double.NegativeInfinity,
Double.PositiveInfinity).flatMap(d =>
implementations.map(impl => impl -> SimpleDouble(d))): _*
)
forAll(simpleDoubleInputs) { (impl: ParseCaseClass, input: SimpleDouble) =>
assert(impl.to[SimpleDouble](input.toString) === input)
}
}
it should "parse SimpleInt" in {
val simpleIntInputs: TableFor2[ParseCaseClass, SimpleInt] = Table(
("implementation", "SimpleInt"),
Seq(1, 0, 2, -5, -10, Int.MaxValue, Int.MinValue).flatMap(i =>
implementations.map(impl => impl -> SimpleInt(i))): _*
)
forAll(simpleIntInputs) { (impl: ParseCaseClass, input: SimpleInt) =>
assert(impl.to[SimpleInt](input.toString) === input)
}
}
it should "parse Int" in {
val simpleIntInputs: TableFor2[ParseCaseClass, Int] = Table(
("implementation", "Int"),
Seq(1, 0, 2, -5, -10, Int.MaxValue, Int.MinValue).flatMap(i =>
implementations.map(impl => impl -> i)): _*
)
forAll(simpleIntInputs) { (impl: ParseCaseClass, input: Int) =>
assert(impl.to[Int](input.toString) === input)
}
}
it should "parse SimpleBoolean" in {
val simpleIntInputs: TableFor2[ParseCaseClass, SimpleBoolean] = Table(
("implementation", "SimpleBoolean"),
Seq(false, true).flatMap(b =>
implementations.map(impl => impl -> SimpleBoolean(b))): _*
)
forAll(simpleIntInputs) { (impl: ParseCaseClass, input: SimpleBoolean) =>
assert(impl.to[SimpleBoolean](input.toString) === input)
}
}
it should "parse SimpleObject" in {
forAll(implementations) { impl => assert(impl.to[SimpleObject.type](SimpleObject.toString) === SimpleObject) }
}
it should "parse options" in {
val options = Table(("implementation", "option"), (for {opt <- Seq(None, Some(4))
impl <- implementations} yield impl -> opt): _*)
forAll(options) { (impl, input) => assert(impl.to[Option[Int]](input.toString) === input) }
}
it should "parse eithers" in {
val options = Table(("implementation", "either"), (for {either <- Seq(Left(LocalDate.of(2018, 4, 2)), Right(3.14159f))
impl <- implementations} yield impl -> either): _*)
forAll(options) { (impl, input) => assert(impl.to[Either[LocalDate, Float]](input.toString) === input) }
}
it should "parse Tuple1s" in {
val options = Table(("implementation", "tuple1"), (for {tup1 <- Seq(Tuple1(Some(2)), Tuple1(None))
impl <- implementations} yield impl -> tup1): _*)
forAll(options) { (impl, input) => assert(impl.to[Tuple1[Option[Int]]](input.toString) === input) }
}

"FastParseParseCaseClass" should "support reuse" in {
val simpleBooleanParser = FastParseParseCaseClass[SimpleBoolean]
assert(simpleBooleanParser.parse("SimpleBoolean(false)") === SimpleBoolean(false))
assert(simpleBooleanParser.parse("SimpleBoolean(true)") === SimpleBoolean(true))
}
}
70 changes: 70 additions & 0 deletions src/test/scala/com/github/aborg0/caseyclassy/FPTwoArgsTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.github.aborg0.caseyclassy

import java.time.{LocalDate, LocalTime}

import com.github.aborg0.caseyclassy.example.TwoArgsBoolInt
import org.scalatest.FlatSpec
import org.scalatest.prop.{TableDrivenPropertyChecks, TableFor1, TableFor2}

class FPTwoArgsTests extends FlatSpec with TableDrivenPropertyChecks {
val implementations: TableFor1[ParseCaseClass] = Table("implementation", FastParseParseCaseClass)

import FastParseParseCaseClass._
behavior of "FastParseParseCaseClass for two args cases"
it should "parse Tuple2[Int, LocalDate]" in {
val intDateInputs: TableFor2[ParseCaseClass, (Int, LocalDate)] = Table(
("implementation", "(Int, LocalDate)"),
Seq(1,
0,
-11111111,
).flatMap(i => Seq(LocalDate.of(1970, 1, 1), LocalDate.of(2000, 12, 31)).map((i, _))).flatMap(tup =>
implementations.map(impl => impl -> tup)): _*
)
forAll(intDateInputs) { (impl: ParseCaseClass, input: (Int, LocalDate)) =>
assert(impl.to[(Int, LocalDate)](input.toString) === input)
}
}
it should "parse Tuple2[Byte, LocalTime]" in {
val byteTimeInputs: TableFor2[ParseCaseClass, (Byte, LocalTime)] = Table(
("implementation", "(Byte, LocalTime)"),
Seq(1.toByte,
0.toByte,
Byte.MinValue,
).flatMap(i => Seq(LocalTime.of(0, 0, 0), LocalTime.of(23, 59, 59)).map((i, _))).flatMap(tup =>
implementations.map(impl => impl -> tup)): _*
)
forAll(byteTimeInputs) { (impl: ParseCaseClass, input: (Byte, LocalTime)) =>
assert(impl.to[(Byte, LocalTime)](input.toString) === input)
}
}
it should "parse Tuple2[Float, Long]" in {
val floatLongInputs: TableFor2[ParseCaseClass, (Float, Long)] = Table(
("implementation", "(Float, Long)"),
Seq(1l,
0l,
Long.MinValue,
Long.MaxValue,
).flatMap(i => Seq(Float.NegativeInfinity, -3.14159f).map((_, i))).flatMap(tup =>
implementations.map(impl => impl -> tup)): _*
)
forAll(floatLongInputs) { (impl: ParseCaseClass, input: (Float, Long)) =>
assert(impl.to[(Float, Long)](input.toString) === input)
}
}

it should "parse TwoArgsBoolInt" in {
val simpleDoubleInputs: TableFor2[ParseCaseClass, TwoArgsBoolInt] = Table(
("implementation", "TwoArgsBoolInt"),
Seq(1,
0,
-11111111,
).flatMap(i => Seq(true, false).map((i, _))).flatMap(tup =>
implementations.map(impl => impl -> TwoArgsBoolInt(tup._2, tup._1))): _*
)
forAll(simpleDoubleInputs) { (impl: ParseCaseClass, input: TwoArgsBoolInt) =>
assert(impl.to[TwoArgsBoolInt](input.toString) === input)
}

}

}
Loading