Skip to content

Commit

Permalink
Merge pull request #616 from finagle/vk/fix-encode-as
Browse files Browse the repository at this point in the history
Fix priority of AnsyncStream encoders
  • Loading branch information
vkostyukov committed Jul 16, 2016
2 parents f215721 + 9dca223 commit cfb0a09
Show file tree
Hide file tree
Showing 7 changed files with 64 additions and 45 deletions.
2 changes: 1 addition & 1 deletion core/src/main/scala/io/finch/Encode.scala
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ object Encode extends LowPriorityEncodeInstances {
)

implicit val encodeExceptionAsJson: Json[Exception] = json(
(e, cs) => BufText(s"""{"message": "${Option(e.getMessage).getOrElse("")}"""", cs)
(e, cs) => BufText(s"""{"message":"${Option(e.getMessage).getOrElse("")}"}""", cs)
)

implicit val encodeString: Text[String] =
Expand Down
11 changes: 5 additions & 6 deletions core/src/main/scala/io/finch/internal/ToResponse.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ trait LowPriorityToResponseInstances {
def apply(a: A, cs: Charset): Response = fn(a, cs)
}

private[this] def asyncResponseBuilder[A, CT <: String](writer: (A, Charset) => Buf)(implicit
protected def asyncResponseBuilder[A, CT <: String](writer: (A, Charset) => Buf)(implicit
w: Witness.Aux[CT]
): Aux[AsyncStream[A], CT] = instance { (as, cs) =>
val rep = Response()
Expand All @@ -42,22 +42,21 @@ trait LowPriorityToResponseInstances {
implicit def asyncBufToResponse[CT <: String](implicit
w: Witness.Aux[CT]
): Aux[AsyncStream[Buf], CT] = asyncResponseBuilder((a, _) => a)
}

trait HighPriorityToResponseInstances extends LowPriorityToResponseInstances {

private[this] val newLine: Buf = Buf.Utf8("\n")

implicit def jsonAsyncStreamToResponse[A](implicit
e: Encode.Json[A],
w: Witness.Aux[Application.Json]
e: Encode.Json[A]
): Aux[AsyncStream[A], Application.Json] =
asyncResponseBuilder((a, cs) => e(a, cs).concat(newLine))

implicit def textAsyncStreamToResponse[A](implicit
e: Encode.Text[A]
): Aux[AsyncStream[A], Text.Plain] =
asyncResponseBuilder((a, cs) => e(a, cs).concat(newLine))
}

trait HighPriorityToResponseInstances extends LowPriorityToResponseInstances {

implicit def responseToResponse[CT <: String]: Aux[Response, CT] = instance((r, _) => r)

Expand Down
21 changes: 12 additions & 9 deletions core/src/test/scala/io/finch/EncodeLaws.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,29 @@ import cats.Eq
import cats.laws._
import cats.laws.discipline._
import cats.std.AllInstances
import com.twitter.io.{Buf, Charsets}
import com.twitter.io.Buf
import io.finch.internal.BufText
import java.nio.charset.Charset
import org.scalacheck.{Arbitrary, Prop}
import org.typelevel.discipline.Laws

trait EncodeLaws[A, CT <: String] extends Laws with MissingInstances with AllInstances {

def encode: Encode.Aux[A, CT]

def roundTrip(a: A): IsEq[Buf] =
encode(a, Charsets.Utf8) <-> Buf.Utf8(a.toString)
def roundTrip(a: A, cs: Charset): IsEq[Buf] =
encode(a, cs) <-> BufText(a.toString, cs)

def all(implicit A: Arbitrary[A], eq: Eq[A]): RuleSet = new DefaultRuleSet(
name = "all",
parent = None,
"roundTrip" -> Prop.forAll { (a: A) => roundTrip(a) }
)
def all(implicit A: Arbitrary[A], CS: Arbitrary[Charset], eq: Eq[A]): RuleSet =
new DefaultRuleSet(
name = "all",
parent = None,
"roundTrip" -> Prop.forAll { (a: A, cs: Charset) => roundTrip(a, cs) }
)
}

object EncodeLaws {
def textPlain[A: Encode.Text]: EncodeLaws[A, Text.Plain] =
def text[A: Encode.Text]: EncodeLaws[A, Text.Plain] =
new EncodeLaws[A, Text.Plain] {
val encode: Encode.Aux[A, Text.Plain] = implicitly[Encode.Text[A]]
}
Expand Down
37 changes: 32 additions & 5 deletions core/src/test/scala/io/finch/EncodeSpec.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,38 @@
package io.finch

import java.nio.charset.Charset
import java.util.UUID

import com.twitter.io.Buf
import io.finch.internal.BufText

class EncodeSpec extends FinchSpec {
checkAll("Encode.TextPlain[String]", EncodeLaws.textPlain[String].all)
checkAll("Encode.TextPlain[Int]", EncodeLaws.textPlain[Int].all)
checkAll("Encode.TextPlain[Option[Boolean]]", EncodeLaws.textPlain[Option[Boolean]].all)
checkAll("Encode.TextPlain[List[Long]]", EncodeLaws.textPlain[List[Long]].all)
checkAll("Encode.TextPlain[Either[UUID, Float]]", EncodeLaws.textPlain[Either[UUID, Float]].all)
checkAll("Encode.Text[String]", EncodeLaws.text[String].all)
checkAll("Encode.Text[Int]", EncodeLaws.text[Int].all)
checkAll("Encode.Text[Option[Boolean]]", EncodeLaws.text[Option[Boolean]].all)
checkAll("Encode.Text[List[Long]]", EncodeLaws.text[List[Long]].all)
checkAll("Encode.Text[Either[UUID, Float]]", EncodeLaws.text[Either[UUID, Float]].all)

it should "round trip Unit" in {
check { (ct: String, cs: Charset) =>
Encode[Unit](ct).apply((), cs) === Buf.Empty
}
}

it should "round trip Buf" in {
check { (ct: String, cs: Charset, buf: Buf) =>
Encode[Buf](ct).apply(buf, cs) === buf
}
}

it should "encode exceptions" in {
check { (s: String, cs: Charset) =>
val e = new Exception(s)

val json = Encode[Exception]("application/json").apply(e, cs)
val text = Encode[Exception]("text/plain").apply(e, cs)

json === BufText(s"""{"message":"$s"}""", cs) && text === BufText(s, cs)
}
}
}
3 changes: 1 addition & 2 deletions core/src/test/scala/io/finch/EndpointSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import java.util.UUID

import cats.Applicative
import cats.laws.discipline.AlternativeTests
import cats.laws.discipline.eq._
import com.twitter.finagle.http.{Request, Method, Cookie}
import com.twitter.util.{Throw, Try, Future}

Expand Down Expand Up @@ -134,7 +133,7 @@ class EndpointSpec extends FinchSpec {
it should "match the entire input" in {
check { i: Input =>
val e = i.path.map(s => s: Endpoint0).foldLeft[Endpoint0](/)((acc, e) => acc :: e)
e(i).remainder == Some(i.copy(path = Nil))
e(i).remainder === Some(i.copy(path = Nil))
}
}

Expand Down
6 changes: 4 additions & 2 deletions core/src/test/scala/io/finch/FinchSpec.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package io.finch

import scala.collection.JavaConverters._
import java.util.UUID
import java.nio.charset.Charset
import java.util.UUID

import cats.Alternative
import cats.Eq
import cats.std.AllInstances
Expand Down Expand Up @@ -212,6 +212,8 @@ trait FinchSpec extends FlatSpec with Matchers with Checkers with AllInstances

implicit def arbitraryCharset: Arbitrary[Charset] = Arbitrary(genCharset)

implicit def arbitraryBuf: Arbitrary[Buf] = Arbitrary(genBuf)

implicit def arbitraryOptionalNonEmptyString: Arbitrary[OptionalNonEmptyString] =
Arbitrary(genOptionalNonEmptyString)

Expand Down
29 changes: 9 additions & 20 deletions examples/src/main/scala/io/finch/streaming/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@ package io.finch.streaming

import java.util.concurrent.atomic.AtomicLong

import cats.Show
import cats.std.long._
import com.twitter.concurrent.AsyncStream
import com.twitter.finagle.{Http, Service}
import com.twitter.finagle.http.{Request, Response, Status}
import com.twitter.finagle.Http
import com.twitter.io.Buf
import com.twitter.util.{Await, Future, Try}
import io.circe.generic.auto._
import com.twitter.util.{Await, Try}
import io.finch._
import io.finch.circe._

/**
* A simple Finch application featuring very basic, `Buf`-based streaming support.
Expand All @@ -31,6 +29,9 @@ object Main extends App {

// An examples domain type.
case class Example(x: Int)
object Example {
implicit val show: Show[Example] = Show.fromToString
}

val sum: AtomicLong = new AtomicLong(0)

Expand Down Expand Up @@ -59,7 +60,8 @@ object Main extends App {
Ok(loop(1, 0))
}

val exampleGenerator: Endpoint[AsyncStream[Example]] = get("examples" :: int) { num: Int =>
// This endpoint will stream back a given number of `Example` objects in plain/text.
val examples: Endpoint[AsyncStream[Example]] = get("examples" :: int) { num: Int =>
Ok(AsyncStream.fromSeq(List.tabulate(num)(i => Example(i))))
}

Expand All @@ -72,21 +74,8 @@ object Main extends App {
Ok(as.map(b => sum.addAndGet(bufToLong(b))))
}

val textPlainService = (sumSoFar :+: sumTo :+: totalSum).toServiceAs[Text.Plain]
val jsonService = exampleGenerator.toService

// TOOD: Fix this once we support multiple content-types
val service = new Service[Request, Response] {
override def apply(request: Request): Future[Response] = {
textPlainService(request).flatMap {
case response if response.status == Status.NotFound => jsonService(request)
case response => Future value response
}
}
}

Await.result(Http.server
.withStreaming(enabled = true)
.serve(":8081", service)
.serve(":8081", (sumSoFar :+: sumTo :+: totalSum :+: examples).toServiceAs[Text.Plain])
)
}

0 comments on commit cfb0a09

Please sign in to comment.