From e7c2fdd16361c19bc83947e021743a420f1b911a Mon Sep 17 00:00:00 2001 From: skolbasov Date: Sun, 10 Dec 2017 13:51:21 +0200 Subject: [PATCH] Endpoint metadata ADT and specs --- core/src/main/scala/io/finch/Endpoint.scala | 23 +++++++ .../scala/io/finch/EndpointMetadata.scala | 43 +++++++++++++ core/src/main/scala/io/finch/Endpoints.scala | 6 ++ .../main/scala/io/finch/endpoint/body.scala | 3 + .../main/scala/io/finch/endpoint/cookie.scala | 1 + .../main/scala/io/finch/endpoint/header.scala | 1 + .../scala/io/finch/endpoint/multipart.scala | 1 + .../main/scala/io/finch/endpoint/param.scala | 3 +- .../main/scala/io/finch/endpoint/path.scala | 7 +++ .../io/finch/syntax/EndpointMapper.scala | 2 + .../scala/io/finch/EndpointMetadataSpec.scala | 33 ++++++++++ core/src/test/scala/io/finch/FinchSpec.scala | 63 +++++++++++++++++++ .../scala/io/finch/iteratee/package.scala | 1 + 13 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 core/src/main/scala/io/finch/EndpointMetadata.scala create mode 100644 core/src/test/scala/io/finch/EndpointMetadataSpec.scala diff --git a/core/src/main/scala/io/finch/Endpoint.scala b/core/src/main/scala/io/finch/Endpoint.scala index f859a4b2c..b955354ef 100644 --- a/core/src/main/scala/io/finch/Endpoint.scala +++ b/core/src/main/scala/io/finch/Endpoint.scala @@ -69,6 +69,8 @@ trait Endpoint[A] { self => */ def apply(input: Input): Endpoint.Result[A] + def meta: Endpoint.Meta + /** * Maps this endpoint to the given function `A => B`. */ @@ -91,6 +93,7 @@ trait Endpoint[A] { self => final override def item = self.item final override def toString: String = self.toString + final override def meta: Endpoint.Meta = self.meta } /** @@ -114,6 +117,7 @@ trait Endpoint[A] { self => override def item = self.item final override def toString: String = self.toString + final override def meta: Endpoint.Meta = self.meta } /** @@ -141,6 +145,7 @@ trait Endpoint[A] { self => override def item = self.item final override def toString: String = self.toString + final override def meta: Endpoint.Meta = self.meta } /** @@ -187,6 +192,7 @@ trait Endpoint[A] { self => override def item = self.item final override def toString: String = self.toString + final override def meta: Endpoint.Meta = self.meta } /** @@ -202,6 +208,7 @@ trait Endpoint[A] { self => override def item = items.MultipleItems final override def toString: String = s"${other.toString} :: ${self.toString}" + final override def meta: Endpoint.Meta = EndpointMetadata.Product(other.meta, self.meta) } /** @@ -220,6 +227,7 @@ trait Endpoint[A] { self => override def item = items.MultipleItems final override def toString: String = s"(${self.toString} :+: ${other.toString})" + final override def meta: Endpoint.Meta = EndpointMetadata.Coproduct(other.meta, self.meta) } /** @@ -349,6 +357,7 @@ trait Endpoint[A] { self => override def item = self.item override final def toString: String = self.toString + override final def meta: Endpoint.Meta = self.meta } /** @@ -357,6 +366,7 @@ trait Endpoint[A] { self => final def withToString(ts: => String): Endpoint[A] = new Endpoint[A] { final def apply(input: Input): Endpoint.Result[A] = self(input) final override def toString: String = ts + final override def meta: Endpoint.Meta = self.meta } private[this] def withOutput[B](fn: Output[A] => Output[B]): Endpoint[B] = @@ -370,11 +380,14 @@ object Endpoint { type Result[A] = EndpointResult[A] + type Meta = EndpointMetadata + /** * Creates an empty [[Endpoint]] (an endpoint that never matches) for a given type. */ def empty[A]: Endpoint[A] = new Endpoint[A] { final def apply(input: Input): Result[A] = EndpointResult.Skipped + final def meta: Endpoint.Meta = EndpointMetadata.Empty } /** @@ -383,6 +396,8 @@ object Endpoint { def const[A](a: A): Endpoint[A] = new Endpoint[A] { final def apply(input: Input): Result[A] = EndpointResult.Matched(input, Rerunnable.const(Output.payload(a))) + + final def meta: Endpoint.Meta = EndpointMetadata.Const } /** @@ -400,6 +415,8 @@ object Endpoint { def lift[A](a: => A): Endpoint[A] = new Endpoint[A] { final def apply(input: Input): Result[A] = EndpointResult.Matched(input, Rerunnable(Output.payload(a))) + + final def meta: Endpoint.Meta = EndpointMetadata.Const } /** @@ -408,6 +425,8 @@ object Endpoint { def liftAsync[A](fa: => Future[A]): Endpoint[A] = new Endpoint[A] { final def apply(input: Input): Result[A] = EndpointResult.Matched(input, Rerunnable.fromFuture(fa).map(a => Output.payload(a))) + + final def meta: Endpoint.Meta = EndpointMetadata.Const } /** @@ -422,6 +441,8 @@ object Endpoint { def liftOutput[A](oa: => Output[A]): Endpoint[A] = new Endpoint[A] { final def apply(input: Input): Result[A] = EndpointResult.Matched(input, Rerunnable(oa)) + + final def meta: Endpoint.Meta = EndpointMetadata.Const } /** @@ -431,6 +452,8 @@ object Endpoint { def liftOutputAsync[A](foa: => Future[Output[A]]): Endpoint[A] = new Endpoint[A] { final def apply(input: Input): Result[A] = EndpointResult.Matched(input, Rerunnable.fromFuture(foa)) + + final def meta: Endpoint.Meta = EndpointMetadata.Const } /** diff --git a/core/src/main/scala/io/finch/EndpointMetadata.scala b/core/src/main/scala/io/finch/EndpointMetadata.scala new file mode 100644 index 000000000..575db32e8 --- /dev/null +++ b/core/src/main/scala/io/finch/EndpointMetadata.scala @@ -0,0 +1,43 @@ +package io.finch + +sealed trait Segment + +object Segment { + + case class Part(name: String) extends Segment + + case object Wildcard extends Segment + + case object Empty extends Segment + +} + +sealed trait EndpointMetadata + +object EndpointMetadata { + + case class Method(method: com.twitter.finagle.http.Method, a: EndpointMetadata) extends EndpointMetadata + + case class Path(segment: io.finch.Segment) extends EndpointMetadata + + case class Parameter(name: String) extends EndpointMetadata + + case class Parameters(name: String) extends EndpointMetadata + + case class Header(name: String) extends EndpointMetadata + + case class Cookie(name: String) extends EndpointMetadata + + case object Body extends EndpointMetadata + + case class Multipart(name: String) extends EndpointMetadata + + case class Coproduct(a: EndpointMetadata, b: EndpointMetadata) extends EndpointMetadata + + case class Product(a: EndpointMetadata, b: EndpointMetadata) extends EndpointMetadata + + case object Const extends EndpointMetadata + + case object Empty extends EndpointMetadata + +} diff --git a/core/src/main/scala/io/finch/Endpoints.scala b/core/src/main/scala/io/finch/Endpoints.scala index 16ca34870..5dd0e34b9 100644 --- a/core/src/main/scala/io/finch/Endpoints.scala +++ b/core/src/main/scala/io/finch/Endpoints.scala @@ -23,6 +23,8 @@ trait Endpoints extends Bodies EndpointResult.Matched(input.copy(route = Nil), Rs.OutputHNil) final override def toString: String = "*" + + final def meta: Endpoint.Meta = EndpointMetadata.Path(Segment.Wildcard) } /** @@ -33,6 +35,8 @@ trait Endpoints extends Bodies EndpointResult.Matched(input, Rs.OutputHNil) final override def toString: String = "" + + final def meta: Endpoint.Meta = EndpointMetadata.Path(Segment.Empty) } /** @@ -43,5 +47,7 @@ trait Endpoints extends Bodies EndpointResult.Matched(input, Rs.payload(input.request)) final override def toString: String = "root" + + final def meta: Endpoint.Meta = EndpointMetadata.Const } } diff --git a/core/src/main/scala/io/finch/endpoint/body.scala b/core/src/main/scala/io/finch/endpoint/body.scala index 8efcf1b86..494d588e5 100644 --- a/core/src/main/scala/io/finch/endpoint/body.scala +++ b/core/src/main/scala/io/finch/endpoint/body.scala @@ -28,6 +28,8 @@ private abstract class FullBody[A] extends Endpoint[A] { } final override def item: RequestItem = items.BodyItem + + final def meta: Endpoint.Meta = EndpointMetadata.Body } private object FullBody { @@ -153,5 +155,6 @@ private[finch] trait Bodies { final override def item: RequestItem = items.BodyItem final override def toString: String = "asyncBody" + final def meta: Endpoint.Meta = EndpointMetadata.Body } } diff --git a/core/src/main/scala/io/finch/endpoint/cookie.scala b/core/src/main/scala/io/finch/endpoint/cookie.scala index 126399464..ebab9cf91 100644 --- a/core/src/main/scala/io/finch/endpoint/cookie.scala +++ b/core/src/main/scala/io/finch/endpoint/cookie.scala @@ -17,6 +17,7 @@ private abstract class Cookie[A](name: String) extends Endpoint[A] { final override def item: items.RequestItem = items.CookieItem(name) final override def toString: String = s"cookie($name)" + final def meta: Endpoint.Meta = EndpointMetadata.Cookie(name) } private object Cookie { diff --git a/core/src/main/scala/io/finch/endpoint/header.scala b/core/src/main/scala/io/finch/endpoint/header.scala index cff7b5ab1..7481bf87f 100644 --- a/core/src/main/scala/io/finch/endpoint/header.scala +++ b/core/src/main/scala/io/finch/endpoint/header.scala @@ -25,6 +25,7 @@ private abstract class Header[F[_], A](name: String, d: DecodeEntity[A], tag: Cl final override def item: RequestItem = items.HeaderItem(name) final override def toString: String = s"header($name)" + final def meta: Endpoint.Meta = EndpointMetadata.Header(name) } private object Header { diff --git a/core/src/main/scala/io/finch/endpoint/multipart.scala b/core/src/main/scala/io/finch/endpoint/multipart.scala index 30e986fdc..485b084eb 100644 --- a/core/src/main/scala/io/finch/endpoint/multipart.scala +++ b/core/src/main/scala/io/finch/endpoint/multipart.scala @@ -34,6 +34,7 @@ private abstract class Multipart[A, B](name: String) extends Endpoint[B] { final override def item: RequestItem = ParamItem(name) final override def toString: String = name + final def meta: Endpoint.Meta = EndpointMetadata.Multipart(name) } private object Multipart { diff --git a/core/src/main/scala/io/finch/endpoint/param.scala b/core/src/main/scala/io/finch/endpoint/param.scala index 4ba29c1dc..62fcd099f 100644 --- a/core/src/main/scala/io/finch/endpoint/param.scala +++ b/core/src/main/scala/io/finch/endpoint/param.scala @@ -26,7 +26,7 @@ private abstract class Param[F[_], A](name: String, d: DecodeEntity[A], tag: Cla final override def item: items.RequestItem = items.ParamItem(name) final override def toString: String = s"param($name)" - + final def meta: Endpoint.Meta = EndpointMetadata.Parameter(name) } @@ -75,6 +75,7 @@ private abstract class Params[F[_], A](name: String, d: DecodeEntity[A], tag: Cl } final override def item: items.RequestItem = items.ParamItem(name) final override def toString: String = s"params($name)" + final def meta: Endpoint.Meta = EndpointMetadata.Parameters(name) } private object Params { diff --git a/core/src/main/scala/io/finch/endpoint/path.scala b/core/src/main/scala/io/finch/endpoint/path.scala index a0a843d20..5baa5d349 100644 --- a/core/src/main/scala/io/finch/endpoint/path.scala +++ b/core/src/main/scala/io/finch/endpoint/path.scala @@ -14,6 +14,7 @@ private class MatchPath(s: String) extends Endpoint[HNil] { } final override def toString: String = s + final def meta: Endpoint.Meta = EndpointMetadata.Path(Segment.Part(s)) } private class ExtractPath[A](implicit d: DecodePath[A], ct: ClassTag[A]) extends Endpoint[A] { @@ -28,6 +29,9 @@ private class ExtractPath[A](implicit d: DecodePath[A], ct: ClassTag[A]) extends } final override def toString: String = s":${ct.runtimeClass.getSimpleName.toLowerCase}" + + final def meta: Endpoint.Meta = + EndpointMetadata.Path(Segment.Part(ct.runtimeClass.getSimpleName.toLowerCase)) } private class ExtractPaths[A](implicit d: DecodePath[A], ct: ClassTag[A]) extends Endpoint[Seq[A]] { @@ -38,6 +42,9 @@ private class ExtractPaths[A](implicit d: DecodePath[A], ct: ClassTag[A]) extend ) final override def toString: String = s":${ct.runtimeClass.getSimpleName.toLowerCase}*" + + final def meta: Endpoint.Meta = + EndpointMetadata.Path(Segment.Part(s"${ct.runtimeClass.getSimpleName.toLowerCase}*")) } private[finch] trait Paths { diff --git a/core/src/main/scala/io/finch/syntax/EndpointMapper.scala b/core/src/main/scala/io/finch/syntax/EndpointMapper.scala index dac8dcd5a..2890ce873 100644 --- a/core/src/main/scala/io/finch/syntax/EndpointMapper.scala +++ b/core/src/main/scala/io/finch/syntax/EndpointMapper.scala @@ -15,4 +15,6 @@ class EndpointMapper[A](m: Method, e: Endpoint[A]) extends Endpoint[A] { self => else EndpointResult.Skipped final override def toString: String = s"${ m.toString.toUpperCase } /${ e.toString }" + + final def meta: Endpoint.Meta = EndpointMetadata.Method(m, e.meta) } diff --git a/core/src/test/scala/io/finch/EndpointMetadataSpec.scala b/core/src/test/scala/io/finch/EndpointMetadataSpec.scala new file mode 100644 index 000000000..7c9da2503 --- /dev/null +++ b/core/src/test/scala/io/finch/EndpointMetadataSpec.scala @@ -0,0 +1,33 @@ +package io.finch + +import io.finch.syntax.EndpointMapper + +class EndpointMetadataSpec extends FinchSpec { + + behavior of "EndpointMetadata" + + private def interpreter(ms: EndpointMetadata): Endpoint[_] = ms match { + case EndpointMetadata.Method(m, meta) => new EndpointMapper(m, interpreter(meta)) + case EndpointMetadata.Path(s) => s match { + case Segment.Part(part) => path(part) + case Segment.Wildcard => * + case Segment.Empty => / + } + case EndpointMetadata.Multipart(name) => multipartAttribute(name) + case EndpointMetadata.Cookie(name) => cookie(name) + case EndpointMetadata.Parameter(name) => param(name) + case EndpointMetadata.Parameters(name) => params(name) + case EndpointMetadata.Header(name) => header(name) + case EndpointMetadata.Body => stringBody + case EndpointMetadata.Empty => Endpoint.empty[String] + case EndpointMetadata.Const => Endpoint.const("foo") + case EndpointMetadata.Coproduct(a, b) => interpreter(b) :+: interpreter(a) + case EndpointMetadata.Product(a, b) => interpreter(a) :: interpreter(b) + } + + it should "do a round-trip" in { + check { l: EndpointMetadata => + interpreter(l).meta === l + } + } +} diff --git a/core/src/test/scala/io/finch/FinchSpec.scala b/core/src/test/scala/io/finch/FinchSpec.scala index e5275ae83..62dee321a 100644 --- a/core/src/test/scala/io/finch/FinchSpec.scala +++ b/core/src/test/scala/io/finch/FinchSpec.scala @@ -141,6 +141,63 @@ trait FinchSpec extends FlatSpec with Matchers with Checkers with AllInstances Method.Options, Method.Patch, Method.Post, Method.Put, Method.Trace ) + def genMetadataCoproduct(maxDepth: Int): Gen[EndpointMetadata.Coproduct] = for { + a <- genMetadata(maxDepth - 1) + b <- genMetadata(maxDepth - 1) + } yield { + EndpointMetadata.Coproduct(a, b) + } + + def genMetadataProduct(maxDepth: Int): Gen[EndpointMetadata.Product] = for { + a <- genMetadata(maxDepth - 1) + b <- genMetadata(maxDepth - 1) + } yield { + EndpointMetadata.Product(a, b) + } + + def genMetadataPath: Gen[EndpointMetadata.Path] = Gen.oneOf( + Gen.const(EndpointMetadata.Path(Segment.Empty)), + Gen.const(EndpointMetadata.Path(Segment.Wildcard)), + Gen.alphaStr.map(s => EndpointMetadata.Path(Segment.Part(s))) + ) + + def genMetadataMethod(maxDepth: Int): Gen[EndpointMetadata.Method] = { + for { + method <- genMethod + meta <- genMetadata(maxDepth - 1) + } yield { + EndpointMetadata.Method(method, meta) + } + } + + def genMetadata(maxDepth: Int): Gen[EndpointMetadata] = { + + val const: List[Gen[EndpointMetadata]] = List( + Gen.const(EndpointMetadata.Body), + Gen.const(EndpointMetadata.Const), + Gen.const(EndpointMetadata.Empty), + Gen.alphaStr.map(EndpointMetadata.Parameter.apply), + Gen.alphaStr.map(EndpointMetadata.Parameters.apply), + Gen.alphaStr.map(EndpointMetadata.Header.apply), + Gen.alphaStr.map(EndpointMetadata.Cookie.apply), + Gen.alphaStr.map(EndpointMetadata.Multipart), + genMetadataPath + ) + + val withRecursive: List[Gen[EndpointMetadata]] = if (maxDepth > 0) { + const ++ List( + genMetadataCoproduct(maxDepth), + genMetadataMethod(maxDepth), + genMetadataProduct(maxDepth) + ) + } else { + const + } + + val (a :: b :: tail) = withRecursive + Gen.oneOf(a, b, tail:_*) + } + def genVersion: Gen[Version] = Gen.oneOf(Version.Http10, Version.Http11) def genPath: Gen[Path] = for { @@ -196,6 +253,8 @@ trait FinchSpec extends FlatSpec with Matchers with Checkers with AllInstances new Endpoint[A] { final def apply(input: Input): Endpoint.Result[A] = EndpointResult.Matched(input, Rerunnable(Output.payload(f(input)))) + + final def meta: Endpoint.Meta = EndpointMetadata.Const } } ) @@ -271,4 +330,8 @@ trait FinchSpec extends FlatSpec with Matchers with Checkers with AllInstances implicit def arbitraryErrors: Arbitrary[Errors] = Arbitrary(genErrors) + + implicit def aribtraryMetadata: Arbitrary[EndpointMetadata] = + Arbitrary(genMetadata(maxDepth = 10)) + } diff --git a/iteratee/src/main/scala/io/finch/iteratee/package.scala b/iteratee/src/main/scala/io/finch/iteratee/package.scala index 3973c691b..de18a6f45 100644 --- a/iteratee/src/main/scala/io/finch/iteratee/package.scala +++ b/iteratee/src/main/scala/io/finch/iteratee/package.scala @@ -48,6 +48,7 @@ package object iteratee extends IterateeInstances { final override def item: RequestItem = items.BodyItem final override def toString: String = "enumeratorBody" + final def meta: Endpoint.Meta = EndpointMetadata.Body } }