Skip to content

Commit

Permalink
Prepare 0.11-M3
Browse files Browse the repository at this point in the history
  • Loading branch information
vkostyukov committed Aug 31, 2016
1 parent 82f2f42 commit f3c7644
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 13 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Every Finch module is published at Maven Central. Use the following _sbt_ snippe

```scala
libraryDependencies ++= Seq(
"com.github.finagle" %% "[finch-module]" % "0.11.0-M2"
"com.github.finagle" %% "[finch-module]" % "0.11.0-M3"
)
```

Expand All @@ -51,7 +51,7 @@ libraryDependencies ++= Seq(

Hello World!
------------
This "Hello World!" example is built with the `0.11.0-M2` version of `finch-core`.
This "Hello World!" example is built with the `0.11.0-M3` version of `finch-core`.

```scala
import io.finch._
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ class SuccessfulEndpointBenchmark extends FinchBenchmark with FooEndpointsAndReq
@State(Scope.Benchmark)
class FailingEndpointBenchmark extends FinchBenchmark with FooEndpointsAndRequests {
@Benchmark
def hlistGenericEndpoint: Try[Foo] = hlistGenericFooReader(badFooRequest).poll.get
def hlistGenericEndpoint: Try[Foo] = hlistGenericFooReader(badFooRequest).tryValue.get

@Benchmark
def derivedEndpoint: Try[Foo] = derivedFooReader(badFooRequest).poll.get
def derivedEndpoint: Try[Foo] = derivedFooReader(badFooRequest).tryValue.get
}

/**
Expand Down
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import ScoverageSbtPlugin._

lazy val buildSettings = Seq(
organization := "com.github.finagle",
version := "0.11.0-M2",
version := "0.11.0-M3",
scalaVersion := "2.11.8",
crossScalaVersions := Seq("2.10.6", "2.11.8")
)
Expand Down
62 changes: 59 additions & 3 deletions core/src/main/scala/io/finch/Output.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package io.finch
import cats.Eq
import com.twitter.finagle.http.{Cookie, Response, Status, Version}
import com.twitter.io.Charsets
import com.twitter.util.{Await, Future, Try}
import com.twitter.util.{Await, Duration, Future, Try}
import io.finch.internal.ToResponse
import java.nio.charset.Charset

Expand Down Expand Up @@ -167,10 +167,66 @@ object Output {
case (_, _) => false
}

/**
* Exposes an API for testing [[Endpoint]]s.
*/
implicit class EndpointResultOps[A](val o: Endpoint.Result[A]) extends AnyVal {
def poll: Option[Try[A]] = o.flatMap(_._2.run.poll.map(_.map(_.value)))
def output: Option[Output[A]] = o.map({ case (_, oa) => Await.result(oa.run) })

/**
* Queries an [[Output]] wrapped with [[Try]] (indicating if the [[Future]] is failed).
*
* @note This method is blocking and awaits on the underlying [[Future]] with the upper
* bound of 10 seconds.
*
* @return `Some(output)` if this endpoint was matched on a given input,
* `None` otherwise.
*/
def tryOutput: Option[Try[Output[A]]] =
o.map({ case (_, oa) => Await.result(oa.liftToTry.run, Duration.fromSeconds(10)) })

/**
* Queries a value from the [[Output]] wrapped with [[Try]] (indicating if either the
* [[Future]] is failed or [[Output]] wasn't a payload).
*
* @note This method is blocking and awaits on the underlying [[Future]] with the upper
* bound of 10 seconds.
*
* @return `Some(value)` if this endpoint was matched on a given input,
* `None` otherwise.
*/
def tryValue: Option[Try[A]] =
tryOutput.map(toa => toa.flatMap(oa => Try(oa.value)))

/**
* Queries an [[Output]] of the [[Endpoint]] result or throws an exception if an underlying
* [[Future]] is failed.
*
* @note This method is blocking and awaits on the underlying [[Future]] with the upper
* bound of 10 seconds.
*
* @return `Some(output)` if this endpoint was matched on a given input,
* `None` otherwise.
*/
def output: Option[Output[A]] = tryOutput.map(tao => tao.get)

/**
* Queries the value from the [[Output]] or throws an exception if either an underlying
* [[Future]] is failed or [[Output]] wasn't a payload.
*
* @note This method is blocking and awaits on the underlying [[Future]] with the upper
* bound of 10 seconds.
*
* @return `Some(value)` if this endpoint was matched on a given input,
* `None` otherwise.
*/
def value: Option[A] = output.map(oa => oa.value)

/**
* Returns the remainder of the [[Input]] after an [[Endpoint]] is matched.
*
* @return `Some(remainder)` if this endpoint was matched on a given input,
* `None` otherwise.
*/
def remainder: Option[Input] = o.map(_._1)
}

Expand Down
2 changes: 1 addition & 1 deletion core/src/test/scala/io/finch/EndpointSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ class EndpointSpec extends FinchSpec {
param("foo"), header("foo"), body, cookie("foo").map(_.value),
fileUpload("foo").map(_.fileName), paramsNonEmpty("foo").map(_.mkString),
paramsNel("foor").map(_.toList.mkString), binaryBody.map(new String(_))
).foreach { ii => ii(i).poll shouldBe Some(Throw(Error.NotPresent(ii.item))) }
).foreach { ii => ii(i).tryValue shouldBe Some(Throw(Error.NotPresent(ii.item))) }
}

it should "maps lazily to values" in {
Expand Down
2 changes: 1 addition & 1 deletion docs/best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* [Monitor your application](best-practices.md#monitor-your-application)
* [Picking HTTP statuses for responses](best-practices.md#picking-http-statuses-for-responses)
* [Configuring Finagle](best-practices.md#configuring-finagle)
* [Finagle Filters vs. Finch Endpoints](best-practices.md#finagle-filters-vs-finc-endpoints)
* [Finagle Filters vs. Finch Endpoints](best-practices.md#finagle-filters-vs-finch-endpoints)
* [Pulling it all Together](best-practices.md#pulling-it-all-together)

--
Expand Down
63 changes: 63 additions & 0 deletions docs/endpoint.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* [Validation](endpoint.md#validation)
* [Errors](endpoint.md#errors)
* [Error Handling](endpoint.md#error-handling)
* [Testing Endpoints](endpoint.md#testing-endpoints)

--

Expand Down Expand Up @@ -606,6 +607,68 @@ implicit val e: Encode.Text[Exception] = Encode.text((exception, charset) =>
)
```

### Testing Endpoints

One of the advantages of typeful endpoints in Finch is that they can be unit-tested independently in
a way similar to how functions are tested. The machinery is pretty straightforward: an endpoint
takes an `Input` and returns `Output` (wrapping a payload).

There is a lightweight API around `Input`s to make them easy to build. For example, the following
call builds a `GET /foo?a=1&b=2` request:

```scala
import io.finch._

val foo: Input = Input.get("/foo", "a" -> "2", "b" -> "3")
```

Similarly a payload (`application/x-www-form-urlencoded` in this case) with headers may be added
to an input:

```scala
import io.finch._
val bar: Input = Input.post("/bar")
.withForm("a" -> "1", "b" -> "2")
.withHeaders("X-Header" -> "Y")
```

At this point, there is no JSON-specific support encoded in the `Input` API (but will be in 0.11)
so a generic `withBody` method may be used instead.

```scala
import io.circe._
import io.circe.syntax._
import io.finch._
import com.twitter.io.Buf

val baz: Input = Input.put("/baz")
.withBody(Buf.Utf8(Map("a" -> "b").asJson.toString), Some("application/json;charset=utf8"))
```

Similarly when an `Output` is returned form the `Endpoint` it might be queried with a number of
methods: `tryValue`, `tryOutput`, `output`, and `value`. Please, note that all of those are blocking
on the underlying `Future` with the upper bound of 10 seconds.

In general, it's recommended to use `try*` variants (since they don't throw exceptions), but for the
sake of simplicity, in this user guide `value` and `output` serve sufficiently.

```
scala> val divOrFail: Endpoint[Int] = post(int :: int) { (a: Int, b: Int) =>
| if (b == 0) BadRequest(new Exception("div by 0"))
| else Ok(a / b)
| }
divOrFail: io.finch.Endpoint[Int] = POST /:int/:int
scala> divOrFail(Input.post("/20/10")).value == Some(2)
res3: Boolean = true
scala> divOrFail(Input.get("/20/10")).value == None
res4: Boolean = true
scala> divOrFail(Input.post("/20/0")).output.map(_.status) == Some(Status.BadRequest)
res6: Boolean = true
```

--
Read Next: [Authentication](auth.md)

Expand Down
4 changes: 2 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ build.sbt:

```scala
libraryDependencies ++= Seq(
"com.github.finagle" %% "finch-core" % "0.11.0-M2",
"com.github.finagle" %% "finch-circe" % "0.11.0-M2",
"com.github.finagle" %% "finch-core" % "0.11.0-M3",
"com.github.finagle" %% "finch-circe" % "0.11.0-M3",
"io.circe" %% "circe-generic" % "0.5.0-M2"
)
```
Expand Down
3 changes: 2 additions & 1 deletion examples/src/main/scala/io/finch/oauth2/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ object Main extends App {
ai: AuthInfo[OAuthUser] => Ok(ai.user)
}

val tokens: Endpoint[GrantHandlerResult] = post("users" :: "auth" :: issueAccessToken(InMemoryDataHandler))
val tokens: Endpoint[GrantHandlerResult] =
post("users" :: "auth" :: issueAccessToken(InMemoryDataHandler))

val unprotected: Endpoint[UnprotectedUser] = get("users" :: "unprotected") {
Ok(UnprotectedUser("unprotected"))
Expand Down

0 comments on commit f3c7644

Please sign in to comment.