Skip to content

Commit

Permalink
Merge pull request #695 from finagle/vk/body-with-ct
Browse files Browse the repository at this point in the history
Add new body endpoints
  • Loading branch information
vkostyukov authored Dec 13, 2016
2 parents 6439eed + f36c237 commit 8c0b305
Show file tree
Hide file tree
Showing 14 changed files with 215 additions and 63 deletions.
29 changes: 23 additions & 6 deletions benchmarks/src/main/scala/io/finch/benchmarks/BodyBenchmark.scala
Original file line number Diff line number Diff line change
@@ -1,29 +1,46 @@
package io.finch.benchmarks

import com.twitter.io.Buf
import com.twitter.util.Return
import io.finch._
import io.finch.internal.BufText
import org.openjdk.jmh.annotations.{Benchmark, Scope, State}

@State(Scope.Benchmark)
class BodyBenchmark extends FinchBenchmark {

implicit val decodeJsonAsString: Decode.Json[String] =
Decode.json((b, cs) => Return(BufText.extract(b, cs)))

val input = Input.post("/").withBody[Text.Plain](Buf.Utf8("x" * 1024))

val bodyAsString = body.as[String]
val bodyOptionAsString = bodyOption.as[String]

val bodyAsString2 = body[String, Application.Json]
val bodyOptionAsString2 = bodyOption[String, Application.Json]

@Benchmark
def jsonOption: Option[String] = bodyOptionAsString(input).value.get

@Benchmark
def json: String = bodyAsString(input).value.get

@Benchmark
def bufOption: Option[Buf] = bodyOption(input).value.get
def jsonOption2: Option[String] = bodyOptionAsString2(input).value.get

@Benchmark
def buf: Buf = body(input).value.get
def json2: String = bodyAsString2(input).value.get

@Benchmark
def stringOption: Option[String] = bodyStringOption(input).value.get
def stringOption: Option[String] = stringBodyOption(input).value.get

@Benchmark
def string: String = bodyString(input).value.get
def string: String = stringBody(input).value.get

@Benchmark
def byteArrayOption: Option[Array[Byte]] = bodyByteArrayOption(input).value.get
def byteArrayOption: Option[Array[Byte]] = binaryBodyOption(input).value.get

@Benchmark
def byteArray: Array[Byte] = bodyByteArray(input).value.get
def byteArray: Array[Byte] = binaryBody(input).value.get
}
2 changes: 1 addition & 1 deletion core/src/main/scala/io/finch/Endpoint.scala
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ object Endpoint {
* Creates an [[Endpoint]] that always matches and returns a given value (evaluated eagerly).
*/
def const[A](a: A): Endpoint[A] = new Endpoint[A] {
def apply(input: Input): Result[A] = Some(input -> rerunnable(Output.payload(a)))
def apply(input: Input): Result[A] = Some(input -> Rs.const(Output.payload(a)))
}

/**
Expand Down
79 changes: 50 additions & 29 deletions core/src/main/scala/io/finch/Endpoints.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,17 @@ import com.twitter.finagle.netty3.ChannelBufferBuf
import com.twitter.io.Buf
import com.twitter.util.{Base64StringEncoder, Future, Try}
import io.catbird.util.Rerunnable
import io.finch.endpoint._
import io.finch.internal._
import java.util.UUID
import scala.reflect.ClassTag
import shapeless._

/**
* A collection of [[Endpoint]] combinators.
*/
trait Endpoints {

private[this] val hnilFutureOutput: Rerunnable[Output[HNil]] = new Rerunnable[Output[HNil]] {
override val run = Future.value(Output.payload(HNil))
}

type Endpoint0 = Endpoint[HNil]
type Endpoint2[A, B] = Endpoint[A :: B :: HNil]
type Endpoint3[A, B, C] = Endpoint[A :: B :: C :: HNil]
Expand All @@ -31,11 +29,11 @@ trait Endpoints {
private[finch] class Matcher(s: String) extends Endpoint[HNil] {
def apply(input: Input): Endpoint.Result[HNil] =
input.headOption.flatMap {
case `s` => Some(input.drop(1) -> hnilFutureOutput)
case `s` => Some(input.drop(1) -> Rs.OutputHNil)
case _ => None
}

override def toString = s
override def toString: String = s
}

implicit def stringToMatcher(s: String): Endpoint0 = new Matcher(s)
Expand Down Expand Up @@ -179,7 +177,7 @@ trait Endpoints {
*/
object * extends Endpoint[HNil] {
def apply(input: Input): Endpoint.Result[HNil] =
Some(input.copy(path = Nil) -> hnilFutureOutput)
Some(input.copy(path = Nil) -> Rs.OutputHNil)

override def toString: String = "*"
}
Expand All @@ -189,7 +187,7 @@ trait Endpoints {
*/
object / extends Endpoint[HNil] {
def apply(input: Input): Endpoint.Result[HNil] =
Some(input -> hnilFutureOutput)
Some(input -> Rs.OutputHNil)

override def toString: String = ""
}
Expand Down Expand Up @@ -294,7 +292,7 @@ trait Endpoints {
}

private[this] val someEmptyBuf = Some(Buf.Empty)
private[this] def requestBody(req: Request): Option[Buf] =
private[this] final def requestBody(req: Request): Option[Buf] =
req.contentLength match {
case Some(0) => someEmptyBuf
case Some(_) => Some(req.content)
Expand Down Expand Up @@ -417,50 +415,35 @@ trait Endpoints {
* An evaluating [[Endpoint]] that reads a binary request body, interpreted as a `Array[Byte]`,
* into an `Option`. The returned [[Endpoint]] only matches non-chunked (non-streamed) requests.
*/
val bodyByteArrayOption: Endpoint[Option[Array[Byte]]] =
val binaryBodyOption: Endpoint[Option[Array[Byte]]] =
matches(items.BodyItem)(!_.isChunked)(requestBodyByteArray)

/**
* An evaluating [[Endpoint]] that reads a required binary request body, interpreted as an
* `Array[Byte]`, or throws a [[Error.NotPresent]] exception. The returned [[Endpoint]] only
* matches non-chunked (non-streamed) requests.
*/
val bodyByteArray: Endpoint[Array[Byte]] = bodyByteArrayOption.failIfNone

/**
* An evaluating [[Endpoint]] that reads a binary request body, interpreted as a `Array[Byte]`,
* into an `Option`. The returned [[Endpoint]] only matches non-chunked (non-streamed) requests.
*/
@deprecated("Use bodyByteArrayOption instead.", "0.11")
val binaryBodyOption: Endpoint[Option[Array[Byte]]] = bodyByteArrayOption

/**
* An evaluating [[Endpoint]] that reads a required binary request body, interpreted as an
* `Array[Byte]`, or throws a [[Error.NotPresent]] exception. The returned [[Endpoint]] only
* matches non-chunked (non-streamed) requests.
*/
@deprecated("Use bodyByteArray instead.", "0.11")
val binaryBody: Endpoint[Array[Byte]] = bodyByteArray
val binaryBody: Endpoint[Array[Byte]] = binaryBodyOption.failIfNone

/**
* An evaluating [[Endpoint]] that reads an optional request body, interpreted as a `String`, into
* an `Option`. The returned [[Endpoint]] only matches non-chunked (non-streamed) requests.
*/
val bodyStringOption: Endpoint[Option[String]] =
val stringBodyOption: Endpoint[Option[String]] =
matches(items.BodyItem)(!_.isChunked)(requestBodyString)

/**
* An evaluating [[Endpoint]] that reads the required request body, interpreted as a `String`, or
* throws an [[Error.NotPresent]] exception. The returned [[Endpoint]] only matches non-chunked
* (non-streamed) requests.
*/
val bodyString: Endpoint[String] = bodyStringOption.failIfNone

val stringBody: Endpoint[String] = stringBodyOption.failIfNone

/**
* An [[Endpoint]] that reads an optional request body, interpreted as [[Buf]], into
* an `Option`. The returned [[Endpoint]] only matches non-chunked (non-streamed) requests.
*/
@deprecated("Use bodyOption[A, ContentType] instead", "0.11")
val bodyOption: Endpoint[Option[Buf]] =
matches(items.BodyItem)(!_.isChunked)(requestBody)

Expand All @@ -469,8 +452,46 @@ trait Endpoints {
* throws an [[Error.NotPresent]] exception. The returned [[Endpoint]] only matches non-chunked
* (non-streamed) requests.
*/
@deprecated("Use body[A, ContentType] instead", "0.11")
val body: Endpoint[Buf] = bodyOption.failIfNone

/**
* An [[Endpoint]] that reads an optional request body represented as `CT` (`ContentType`) and
* interpreted as `A`, into an `Option`. The returned [[Endpoint]] only matches non-chunked
* (non-streamed) requests.
*/
def bodyOption[A, CT <: String](implicit
d: Decode.Aux[A, CT], ct: ClassTag[A]): Endpoint[Option[A]] = new OptionalBody[A, CT](d, ct)

/**
* An [[Endpoint]] that reads the required request body represented as `CT` (`ContentType`) and
* interpreted as `A`, or throws an [[Error.NotPresent]] exception. The returned [[Endpoint]]
* only matches non-chunked (non-streamed) requests.
*/
def body[A, CT <: String](implicit
d: Decode.Aux[A, CT], ct: ClassTag[A]): Endpoint[A] = new RequiredBody[A, CT](d, ct)

/**
* Alias for `body[A, Application.Json]`.
*/
def jsonBody[A: Decode.Json : ClassTag]: Endpoint[A] = body[A, Application.Json]

/**
* Alias for `bodyOption[A, Application.Json]`.
*/
def jsonBodyOption[A: Decode.Json : ClassTag]: Endpoint[Option[A]] =
bodyOption[A, Application.Json]

/**
* Alias for `body[A, Text.Plain]`
*/
def textBody[A: Decode.Text : ClassTag]: Endpoint[A] = body[A, Text.Plain]

/**
* Alias for `bodyOption[A, Text.Plain]`
*/
def textBodyOption[A: Decode.Text : ClassTag]: Endpoint[Option[A]] = bodyOption[A, Text.Plain]

/**
* An evaluating [[Endpoint]] that reads a required chunked streaming binary body, interpreted as
* an `AsyncStream[Buf]`. The returned [[Endpoint]] only matches chunked (streamed) requests.
Expand Down
3 changes: 1 addition & 2 deletions core/src/main/scala/io/finch/Error.scala
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ object Error {
}

/**
* An exception that indicates a broken [[[io.finch.request.ValidationRule ValidationRule]] on the
* request item.
* An exception that indicates a broken [[ValidationRule]] on the request item.
*
* @param item the invalid request item
* @param rule the rule description
Expand Down
52 changes: 52 additions & 0 deletions core/src/main/scala/io/finch/endpoint/Body.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.finch.endpoint

import com.twitter.util.{Future, Return, Throw}
import io.catbird.util.Rerunnable
import io.finch._
import io.finch.internal._
import io.finch.items._
import scala.reflect.ClassTag

private[finch] abstract class Body[A, B, CT <: String](
d: Decode.Aux[A, CT], ct: ClassTag[A]) extends Endpoint[B] {

protected def whenNotPresent: Rerunnable[Output[B]]
protected def prepare(a: A): B

private[this] def decode(i: Input): Future[Output[B]] =
d(i.request.content, i.request.charsetOrUtf8) match {
case Return(r) => Future.value(Output.payload(prepare(r)))
case Throw(t) => Future.exception(Error.NotParsed(items.BodyItem, ct, t))
}

final def apply(input: Input): Endpoint.Result[B] =
if (input.request.isChunked) None
else {
val rr = input.request.contentLength match {
case None => whenNotPresent
case _ => Rerunnable.fromFuture(decode(input))
}

Some(input -> rr)
}

override def item: RequestItem = items.BodyItem
override def toString: String = "body"
}

private[finch] final class RequiredBody[A, CT <: String](
d: Decode.Aux[A, CT], ct: ClassTag[A]) extends Body[A, A, CT](d, ct) {

protected def prepare(a: A): A = a
protected def whenNotPresent: Rerunnable[Output[A]] =
Rs.BodyNotPresent.asInstanceOf[Rerunnable[Output[A]]]
}

private[finch] final class OptionalBody[A, CT <: String](
d: Decode.Aux[A, CT], ct: ClassTag[A]) extends Body[A, Option[A], CT](d, ct) {

protected def prepare(a: A): Option[A] = Some(a)
protected def whenNotPresent: Rerunnable[Output[Option[A]]] =
Rs.OutputNone.asInstanceOf[Rerunnable[Output[Option[A]]]]
}

33 changes: 33 additions & 0 deletions core/src/main/scala/io/finch/internal/Rs.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.finch.internal

import com.twitter.util.Future
import io.catbird.util.Rerunnable
import io.finch._
import io.finch.items._
import shapeless.HNil

/**
* Predefined, Finch-specific instances of [[Rerunnable]].
*/
private[finch] object Rs {
// See https://github.com/travisbrown/catbird/pull/32
final def const[A](a: A): Rerunnable[A] = new Rerunnable[A] {
override def run: Future[A] = Future.value(a)
}

final val OutputNone: Rerunnable[Output[Option[Nothing]]] =
new Rerunnable[Output[Option[Nothing]]] {
override val run: Future[Output[Option[Nothing]]] =
Future.value(Output.None)
}

final val BodyNotPresent: Rerunnable[Nothing] = new Rerunnable[Nothing] {
override val run: Future[Nothing] =
Future.exception(Error.NotPresent(BodyItem))
}

final val OutputHNil: Rerunnable[Output[HNil]] =
new Rerunnable[Output[HNil]] {
override val run: Future[Output[HNil]] = Future.value(Output.payload(HNil))
}
}
5 changes: 0 additions & 5 deletions core/src/main/scala/io/finch/internal/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,6 @@ import java.nio.charset.{Charset, StandardCharsets}
*/
package object internal {

// See https://github.com/travisbrown/catbird/pull/32
def rerunnable[A](a: A): Rerunnable[A] = new Rerunnable[A] {
override def run: Future[A] = Future.value(a)
}

@inline private[this] final val someTrue: Option[Boolean] = Some(true)
@inline private[this] final val someFalse: Option[Boolean] = Some(false)

Expand Down
45 changes: 38 additions & 7 deletions core/src/test/scala/io/finch/BodySpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,50 @@ package io.finch

import java.util.UUID

import com.twitter.finagle.http.Request
import com.twitter.io.Buf
import com.twitter.util.{Return, Throw}
import io.finch.data.Foo

class BodySpec extends FinchSpec {

behavior of "body*"

def withBody(b: String): Input = Input.post("/").withBody[Text.Plain](Buf.Utf8(b))

checkAll("Body[String]", EntityEndpointLaws[String](bodyStringOption)(withBody).evaluating)
checkAll("Body[Int]", EntityEndpointLaws[Int](bodyStringOption)(withBody).evaluating)
checkAll("Body[Long]", EntityEndpointLaws[Long](bodyStringOption)(withBody).evaluating)
checkAll("Body[Boolean]", EntityEndpointLaws[Boolean](bodyStringOption)(withBody).evaluating)
checkAll("Body[Float]", EntityEndpointLaws[Float](bodyStringOption)(withBody).evaluating)
checkAll("Body[Double]", EntityEndpointLaws[Double](bodyStringOption)(withBody).evaluating)
checkAll("Body[UUID]", EntityEndpointLaws[UUID](bodyStringOption)(withBody).evaluating)
checkAll("Body[String]", EntityEndpointLaws[String](stringBodyOption)(withBody).evaluating)
checkAll("Body[Int]", EntityEndpointLaws[Int](stringBodyOption)(withBody).evaluating)
checkAll("Body[Long]", EntityEndpointLaws[Long](stringBodyOption)(withBody).evaluating)
checkAll("Body[Boolean]", EntityEndpointLaws[Boolean](stringBodyOption)(withBody).evaluating)
checkAll("Body[Float]", EntityEndpointLaws[Float](stringBodyOption)(withBody).evaluating)
checkAll("Body[Double]", EntityEndpointLaws[Double](stringBodyOption)(withBody).evaluating)
checkAll("Body[UUID]", EntityEndpointLaws[UUID](stringBodyOption)(withBody).evaluating)

it should "respond with NotFound when it's required" in {
body[Foo, Text.Plain].apply(Input.get("/")).tryValue ===
Some(Throw(Error.NotPresent(items.BodyItem)))
}

it should "respond with None when it's optional" in {
body[Foo, Text.Plain].apply(Input.get("/")).tryValue === Some(Return(None))
}

it should "not match on streaming requests" in {
val req = Request()
req.setChunked(true)
body[Foo, Text.Plain].apply(Input.request(req)).value === None
}

it should "respond with a value when present and required" in {
check { f: Foo =>
body[Foo, Text.Plain].apply(Input.post("/").withBody[Text.Plain](f)).value === Some(f)
}
}

it should "respond with Some(value) when it's present and optional" in {
check { f: Foo =>
bodyOption[Foo, Text.Plain].apply(Input.post("/").withBody[Text.Plain](f)).value ===
Some(Some(f))
}
}
}
Loading

0 comments on commit 8c0b305

Please sign in to comment.