diff --git a/build.sbt b/build.sbt index 86c8abbe3..efd738789 100644 --- a/build.sbt +++ b/build.sbt @@ -161,6 +161,7 @@ lazy val finch = project.in(file(".")) """ |import io.finch._ |import io.finch.circe._ + |import io.finch.generic._ |import io.finch.items._ |import com.twitter.util.{Future, Await} |import com.twitter.concurrent.AsyncStream @@ -178,15 +179,20 @@ lazy val finch = project.in(file(".")) "io.spray" %% "spray-json" % sprayVersion )) .aggregate( - core, argonaut, jackson, json4s, circe, playjson, sprayjson, benchmarks, test, jsonTest, oauth2, - examples, sse + core, generic, argonaut, jackson, json4s, circe, playjson, sprayjson, benchmarks, test, jsonTest, + oauth2, examples, sse ) - .dependsOn(core, circe) + .dependsOn(core, generic, circe) lazy val core = project .settings(moduleName := "finch-core") .settings(allSettings) + lazy val generic = project + .settings(moduleName := "finch-generic") + .settings(allSettings) + .dependsOn(core % "compile->compile;test->test") + lazy val test = project .settings(moduleName := "finch-test") .settings(allSettings) diff --git a/core/src/main/scala/io/finch/Endpoint.scala b/core/src/main/scala/io/finch/Endpoint.scala index 2006e62bd..aef04f7c2 100644 --- a/core/src/main/scala/io/finch/Endpoint.scala +++ b/core/src/main/scala/io/finch/Endpoint.scala @@ -582,18 +582,6 @@ object Endpoint { } } - class GenericDerivation[A] { - def fromParams[Repr <: HList](implicit - gen: LabelledGeneric.Aux[A, Repr], - fp: FromParams[Repr] - ): Endpoint[A] = fp.endpoint.map(gen.from) - } - - /** - * Generically derive a very basic instance of [[Endpoint]] for a given type `A`. - */ - def derive[A]: GenericDerivation[A] = new GenericDerivation[A] - implicit val endpointInstance: Alternative[Endpoint] = new Alternative[Endpoint] { final override def ap[A, B](ff: Endpoint[A => B])(fa: Endpoint[A]): Endpoint[B] = ff.productWith(fa)((f, a) => f(a)) diff --git a/core/src/main/scala/io/finch/internal/FromParams.scala b/generic/src/main/scala/io/finch/generic/FromParams.scala similarity index 95% rename from core/src/main/scala/io/finch/internal/FromParams.scala rename to generic/src/main/scala/io/finch/generic/FromParams.scala index 60f419897..1fec200e0 100644 --- a/core/src/main/scala/io/finch/internal/FromParams.scala +++ b/generic/src/main/scala/io/finch/generic/FromParams.scala @@ -1,4 +1,4 @@ -package io.finch.internal +package io.finch.generic import scala.reflect.ClassTag @@ -32,7 +32,7 @@ object FromParams { } } -private[internal] object Extractor extends Poly1 { +private[generic] object Extractor extends Poly1 { implicit def optionalExtractor[V](implicit dh: DecodeEntity[V], diff --git a/generic/src/main/scala/io/finch/generic/GenericDerivation.scala b/generic/src/main/scala/io/finch/generic/GenericDerivation.scala new file mode 100644 index 000000000..f2b1b06af --- /dev/null +++ b/generic/src/main/scala/io/finch/generic/GenericDerivation.scala @@ -0,0 +1,11 @@ +package io.finch.generic + +import io.finch._ +import shapeless._ + +final class GenericDerivation[A] { + def fromParams[Repr <: HList](implicit + gen: LabelledGeneric.Aux[A, Repr], + fp: FromParams[Repr] + ): Endpoint[A] = fp.endpoint.map(gen.from) +} diff --git a/generic/src/main/scala/io/finch/generic/package.scala b/generic/src/main/scala/io/finch/generic/package.scala new file mode 100644 index 000000000..64176378a --- /dev/null +++ b/generic/src/main/scala/io/finch/generic/package.scala @@ -0,0 +1,8 @@ +package io.finch + +package object generic { + /** + * Generically derive a very basic instance of [[Endpoint]] for a given type `A`. + */ + def deriveEndpoint[A]: GenericDerivation[A] = new GenericDerivation[A] +} diff --git a/generic/src/test/scala/io/finch/generic/DerivedEndpointLaws.scala b/generic/src/test/scala/io/finch/generic/DerivedEndpointLaws.scala new file mode 100644 index 000000000..bd8a3afcd --- /dev/null +++ b/generic/src/test/scala/io/finch/generic/DerivedEndpointLaws.scala @@ -0,0 +1,36 @@ +package io.finch + +import cats.Eq +import cats.instances.AllInstances +import cats.laws._ +import cats.laws.discipline._ +import org.scalacheck.{Arbitrary, Prop} +import org.typelevel.discipline.Laws + +trait DerivedEndpointLaws[A] extends Laws with MissingInstances with AllInstances { + + def endpoint: Endpoint[A] + def toParams: A => Seq[(String, String)] + + def roundTrip(a: A): IsEq[A] = { + val i = Input.get("/", toParams(a): _*) + endpoint(i).awaitValueUnsafe().get <-> a + } + + def evaluating(implicit A: Arbitrary[A], eq: Eq[A]): RuleSet = + new DefaultRuleSet( + name = "evaluating", + parent = None, + "roundTrip" -> Prop.forAll { (a: A) => roundTrip(a) } + ) +} + +object DerivedEndpointLaws { + def apply[A]( + e: Endpoint[A], + tp: A => Seq[(String, String)] + ): DerivedEndpointLaws[A] = new DerivedEndpointLaws[A] { + val endpoint: Endpoint[A] = e + val toParams = tp + } +} diff --git a/generic/src/test/scala/io/finch/generic/GenericSpec.scala b/generic/src/test/scala/io/finch/generic/GenericSpec.scala new file mode 100644 index 000000000..1c9ceaefa --- /dev/null +++ b/generic/src/test/scala/io/finch/generic/GenericSpec.scala @@ -0,0 +1,28 @@ +package io.finch.generic + +import cats.kernel.Eq +import io.finch._ +import org.scalacheck.Arbitrary + +class GenericSpec extends FinchSpec { + + behavior of "generic" + + case class Foo(a: String, b: Int) + + val e: Endpoint[Foo] = deriveEndpoint[Foo].fromParams + + implicit val eq: Eq[Foo] = Eq.fromUniversalEquals + + implicit val arbitraryFoo: Arbitrary[Foo] = Arbitrary(for { + s <- Arbitrary.arbitrary[String] + i <- Arbitrary.arbitrary[Int] + } yield Foo(s, i)) + + val f: Foo => Seq[(String, String)] = foo => Seq( + ("a" -> foo.a), + ("b" -> foo.b.toString) + ) + + checkAll("DerivedEndpoint[Foo]", DerivedEndpointLaws[Foo](e, f).evaluating) +}