From 681e14b3fbb22c063eb42028b0181f7843d2f246 Mon Sep 17 00:00:00 2001 From: Gabor Bakos Date: Sun, 3 Jun 2018 19:14:14 +0200 Subject: [PATCH 1/2] :sparkles: :heavy_plus_sign: Initial version with FastParse Also removed requirement for `AnyRef` in type param. --- build.sbt | 2 + .../caseyclassy/FastParseParseCaseClass.scala | 75 +++++++++++++ .../aborg0/caseyclassy/ParseCaseClass.scala | 2 +- .../caseyclassy/RegexParseCaseClass.scala | 2 +- .../aborg0/caseyclassy/FPSimpleTests.scala | 88 +++++++++++++++ .../aborg0/caseyclassy/FPTwoArgsTests.scala | 70 ++++++++++++ .../aborg0/caseyclassy/FPVarArgsTests.scala | 104 ++++++++++++++++++ .../aborg0/caseyclassy/SimpleTests.scala | 1 + 8 files changed, 342 insertions(+), 2 deletions(-) create mode 100644 src/main/scala/com/github/aborg0/caseyclassy/FastParseParseCaseClass.scala create mode 100644 src/test/scala/com/github/aborg0/caseyclassy/FPSimpleTests.scala create mode 100644 src/test/scala/com/github/aborg0/caseyclassy/FPTwoArgsTests.scala create mode 100644 src/test/scala/com/github/aborg0/caseyclassy/FPVarArgsTests.scala diff --git a/build.sbt b/build.sbt index 686da0a..c3e3157 100644 --- a/build.sbt +++ b/build.sbt @@ -103,3 +103,5 @@ resolvers ++= Seq( libraryDependencies ++= Seq( "com.chuusai" %%% "shapeless" % "2.3.3" ) + +libraryDependencies += "com.lihaoyi" %%% "fastparse" % "1.0.0" diff --git a/src/main/scala/com/github/aborg0/caseyclassy/FastParseParseCaseClass.scala b/src/main/scala/com/github/aborg0/caseyclassy/FastParseParseCaseClass.scala new file mode 100644 index 0000000..fe3ddf7 --- /dev/null +++ b/src/main/scala/com/github/aborg0/caseyclassy/FastParseParseCaseClass.scala @@ -0,0 +1,75 @@ +package com.github.aborg0.caseyclassy + +import fastparse.all._ +import java.time.{LocalDate, LocalTime} + +import fastparse.all +import shapeless._ + +import scala.reflect.runtime.universe._ + +private[caseyclassy] trait FPGenericImplementations { + implicit def longParse: FastParseParse[Long] = () => P("-".? ~ CharIn('0' to '9').rep(1)).!.map(_.toLong) + + implicit def intParse: FastParseParse[Int] = () => P("-".? ~ CharIn('0' to '9').rep(1)).!.map(_.toInt) + + implicit def shortParse: FastParseParse[Short] = () => P("-".? ~ CharIn('0' to '9').rep(1)).!.map(_.toShort) + + implicit def byteParse: FastParseParse[Byte] = () => P("-".? ~ CharIn('0' to '9').rep(1)).!.map(_.toByte) + + implicit def booleanParse: FastParseParse[Boolean] = () => P("true" | "false").!.map(_.toBoolean) + + implicit def doubleParse: FastParseParse[Double] = () => numeric.!.map(_.toDouble) + + implicit def floatParse: FastParseParse[Float] = () => numeric.!.map(_.toFloat) + + private[this] def numeric: all.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 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: TypeTag, 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) +} \ No newline at end of file diff --git a/src/main/scala/com/github/aborg0/caseyclassy/ParseCaseClass.scala b/src/main/scala/com/github/aborg0/caseyclassy/ParseCaseClass.scala index 316f18d..884f6dc 100644 --- a/src/main/scala/com/github/aborg0/caseyclassy/ParseCaseClass.scala +++ b/src/main/scala/com/github/aborg0/caseyclassy/ParseCaseClass.scala @@ -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] { diff --git a/src/main/scala/com/github/aborg0/caseyclassy/RegexParseCaseClass.scala b/src/main/scala/com/github/aborg0/caseyclassy/RegexParseCaseClass.scala index 9c8ae69..cff6386 100644 --- a/src/main/scala/com/github/aborg0/caseyclassy/RegexParseCaseClass.scala +++ b/src/main/scala/com/github/aborg0/caseyclassy/RegexParseCaseClass.scala @@ -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) } diff --git a/src/test/scala/com/github/aborg0/caseyclassy/FPSimpleTests.scala b/src/test/scala/com/github/aborg0/caseyclassy/FPSimpleTests.scala new file mode 100644 index 0000000..8962460 --- /dev/null +++ b/src/test/scala/com/github/aborg0/caseyclassy/FPSimpleTests.scala @@ -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)) + } +} diff --git a/src/test/scala/com/github/aborg0/caseyclassy/FPTwoArgsTests.scala b/src/test/scala/com/github/aborg0/caseyclassy/FPTwoArgsTests.scala new file mode 100644 index 0000000..3307220 --- /dev/null +++ b/src/test/scala/com/github/aborg0/caseyclassy/FPTwoArgsTests.scala @@ -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) + } + + } + +} diff --git a/src/test/scala/com/github/aborg0/caseyclassy/FPVarArgsTests.scala b/src/test/scala/com/github/aborg0/caseyclassy/FPVarArgsTests.scala new file mode 100644 index 0000000..3a535d8 --- /dev/null +++ b/src/test/scala/com/github/aborg0/caseyclassy/FPVarArgsTests.scala @@ -0,0 +1,104 @@ +package com.github.aborg0.caseyclassy + +import com.github.aborg0.caseyclassy.example.{OnlyVarArgs, StringPlusVarArgs} +import org.scalatest.WordSpec +import org.scalatest.prop.{TableDrivenPropertyChecks, TableFor1, TableFor2} + +class FPVarArgsTests extends WordSpec with TableDrivenPropertyChecks { + val implementations: TableFor1[ParseCaseClass] = Table("implementation", FastParseParseCaseClass) + + import FastParseParseCaseClass._ + + "ParseCaseClass for variable arity arguments successfully parse" when { + "OnlyVarArgs" should { + "has empty args" in { + forAll( + Table(("implementation", "OnlyVarArgs[Int]"), implementations.map(impl => impl -> OnlyVarArgs[Int]()): _*)) { + (impl: ParseCaseClass, input: OnlyVarArgs[Int]) => + assert(impl.to[OnlyVarArgs[Int]](input.toString) === input) + } + } + "has one argument" in { + val simpleVarArgsInputs: TableFor2[ParseCaseClass, OnlyVarArgs[Int]] = Table( + ("implementation", "OnlyVarArgs[Int]"), + Seq(1, + 0, + -1, + Int.MaxValue, + Int.MinValue).flatMap(i => + implementations.map(impl => impl -> OnlyVarArgs[Int](i))): _* + ) + forAll(simpleVarArgsInputs) { (impl: ParseCaseClass, input: OnlyVarArgs[Int]) => + assert(impl.to[OnlyVarArgs[Int]](input.toString) === input) + } + } + "has two arguments" in { + val varArgsInputs: TableFor2[ParseCaseClass, OnlyVarArgs[Int]] = Table( + ("implementation", "OnlyVarArgs[Int]"), + Seq(1, + 0, + -1, + Int.MaxValue, + Int.MinValue).flatMap(i => + implementations.map(impl => impl -> OnlyVarArgs[Int](i, -i))): _* + ) + forAll(varArgsInputs) { (impl: ParseCaseClass, input: OnlyVarArgs[Int]) => + assert(impl.to[OnlyVarArgs[Int]](input.toString) === input) + } + } + "has three arguments" in { + val varArgsInputs: TableFor2[ParseCaseClass, OnlyVarArgs[Int]] = Table( + ("implementation", "OnlyVarArgs[Int]"), + Seq(1, + 0, + -1, + Int.MaxValue, + Int.MinValue).flatMap(i => + implementations.map(impl => impl -> OnlyVarArgs[Int](3, i, -i))): _* + ) + forAll(varArgsInputs) { (impl: ParseCaseClass, input: OnlyVarArgs[Int]) => + assert(impl.to[OnlyVarArgs[Int]](input.toString) === input) + } + } + } + "StringPlusVarArgs" should { + "with String + no arguments" in { + val simpleStringPlusVarArgs: TableFor2[ParseCaseClass, StringPlusVarArgs[Short]] = Table( + ("implementation", "StringPlusVarArgs[Short]"), + Seq("", "Hello", "3").flatMap(s => implementations.map(impl => impl -> StringPlusVarArgs[Short](s))): _* + ) + forAll(simpleStringPlusVarArgs) { (impl: ParseCaseClass, input: StringPlusVarArgs[Short]) => + assert(impl.to[StringPlusVarArgs[Short]](input.toString) === input) + } + } + "with String + one argument" in { + val stringPlusVarArgs: TableFor2[ParseCaseClass, StringPlusVarArgs[Short]] = Table( + ("implementation", "StringPlusVarArgs[Short]"), + Seq("", "Hello", "3").flatMap(s => Seq(1.toShort, + 0.toShort, + (-1).toShort, + Short.MaxValue, + Short.MinValue).flatMap(i => + implementations.map(impl => impl -> StringPlusVarArgs(s, i)))): _* + ) + forAll(stringPlusVarArgs) { (impl: ParseCaseClass, input: StringPlusVarArgs[Short]) => + assert(impl.to[StringPlusVarArgs[Short]](input.toString) === input) + } + } + "with String + two arguments" in { + val stringPlusVarArgs: TableFor2[ParseCaseClass, StringPlusVarArgs[Short]] = Table( + ("implementation", "StringPlusVarArgs[Short]"), + Seq("", "Hello", "3").flatMap(s => Seq(1.toShort, + 0.toShort, + (-1).toShort, + Short.MaxValue, + Short.MinValue).flatMap(i => + implementations.map(impl => impl -> StringPlusVarArgs(s, i, 2.toShort)))): _* + ) + forAll(stringPlusVarArgs) { (impl: ParseCaseClass, input: StringPlusVarArgs[Short]) => + assert(impl.to[StringPlusVarArgs[Short]](input.toString) === input) + } + } + } + } +} diff --git a/src/test/scala/com/github/aborg0/caseyclassy/SimpleTests.scala b/src/test/scala/com/github/aborg0/caseyclassy/SimpleTests.scala index 7b66da5..08b6f8f 100644 --- a/src/test/scala/com/github/aborg0/caseyclassy/SimpleTests.scala +++ b/src/test/scala/com/github/aborg0/caseyclassy/SimpleTests.scala @@ -22,6 +22,7 @@ class SimpleTests extends FlatSpec with TableDrivenPropertyChecks { .25, -2.5, -0d, + 7e-17, Double.MaxValue, Double.NaN, Double.NegativeInfinity, From cf8d73ffe8ccaded2f451ac2420c2c198f418635 Mon Sep 17 00:00:00 2001 From: Gabor Bakos Date: Thu, 14 Jun 2018 08:00:36 +0200 Subject: [PATCH 2/2] :memo: :art: Updated tut examples with FastParse, code cleanup --- .../caseyclassy/FastParseParseCaseClass.scala | 50 ++++++---- src/main/tut/Examples.md | 95 ++++++++++++++++++- 2 files changed, 124 insertions(+), 21 deletions(-) diff --git a/src/main/scala/com/github/aborg0/caseyclassy/FastParseParseCaseClass.scala b/src/main/scala/com/github/aborg0/caseyclassy/FastParseParseCaseClass.scala index fe3ddf7..d7f86f3 100644 --- a/src/main/scala/com/github/aborg0/caseyclassy/FastParseParseCaseClass.scala +++ b/src/main/scala/com/github/aborg0/caseyclassy/FastParseParseCaseClass.scala @@ -3,44 +3,52 @@ package com.github.aborg0.caseyclassy import fastparse.all._ import java.time.{LocalDate, LocalTime} -import fastparse.all import shapeless._ import scala.reflect.runtime.universe._ private[caseyclassy] trait FPGenericImplementations { - implicit def longParse: FastParseParse[Long] = () => P("-".? ~ CharIn('0' to '9').rep(1)).!.map(_.toLong) + implicit def booleanParse: FastParseParse[Boolean] = () => P("true" | "false").!.map(_.toBoolean) + + private[this] def integral: Parser[String] = P("-".? ~ CharIn('0' to '9').rep(1)).! - implicit def intParse: FastParseParse[Int] = () => P("-".? ~ CharIn('0' to '9').rep(1)).!.map(_.toInt) + 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 shortParse: FastParseParse[Short] = () => P("-".? ~ CharIn('0' to '9').rep(1)).!.map(_.toShort) + implicit def longParse: FastParseParse[Long] = () => integral.map(_.toLong) - implicit def byteParse: FastParseParse[Byte] = () => P("-".? ~ CharIn('0' to '9').rep(1)).!.map(_.toByte) + implicit def intParse: FastParseParse[Int] = () => integral.map(_.toInt) - implicit def booleanParse: FastParseParse[Boolean] = () => P("true" | "false").!.map(_.toBoolean) + 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) - private[this] def numeric: all.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 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 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: TypeTag, 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 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) + if (name.startsWith("Tuple")) + P("(" ~/ argParse.parser() ~ ")").map(gen.from) + else + P(name ~ (("(" ~/ argParse.parser() ~ ")") | argParse.parser())).map(gen.from) } } } @@ -54,7 +62,8 @@ case object FastParseParseCaseClass extends ParseCaseClass with FPGenericImpleme 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 stringParse: FastParseParse[String] = () => + P(CharsWhile(c => c != ',' && c != ')').!) | P("").! implicit def timeParse: FastParseParse[LocalTime] = () => P( CharIn('0' to '9').rep(2, "", 2) ~ ":" ~ @@ -62,14 +71,19 @@ case object FastParseParseCaseClass extends ParseCaseClass with FPGenericImpleme (":" ~/ 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 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 = ", ") ~ ")") + 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) + 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) } \ No newline at end of file diff --git a/src/main/tut/Examples.md b/src/main/tut/Examples.md index a6c659f..d73ae12 100644 --- a/src/main/tut/Examples.md +++ b/src/main/tut/Examples.md @@ -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 @@ -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. \ No newline at end of file +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)))") +```