Skip to content

Commit

Permalink
Merge pull request #913 from finagle/vk/to-response-negotiable
Browse files Browse the repository at this point in the history
 Move NegotiateToResponse under ToResponse.Negotiable
  • Loading branch information
vkostyukov authored Feb 8, 2018
2 parents 8e2e2d4 + d6d3a99 commit 8984fbd
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 138 deletions.
42 changes: 0 additions & 42 deletions core/src/main/scala/io/finch/NegotiateToResponse.scala

This file was deleted.

35 changes: 35 additions & 0 deletions core/src/main/scala/io/finch/ToResponse.scala
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,41 @@ trait HighPriorityToResponseInstances extends LowPriorityToResponseInstances {

object ToResponse extends HighPriorityToResponseInstances {

/**
* Enables server-driven content negotiation with client.
*
* Picks corresponding instance of `ToResponse` according to `Accept` header of a request
*/
trait Negotiable[A, CT] {
def apply(accept: Seq[Accept]): ToResponse.Aux[A, CT]
}

object Negotiable {

implicit def coproductToNegotiable[A, CTH <: String, CTT <: Coproduct](implicit
h: ToResponse.Aux[A, CTH],
t: Negotiable[A, CTT],
a: Accept.Matcher[CTH]
): Negotiable[A, CTH :+: CTT] = new Negotiable[A, CTH :+: CTT] {
def apply(accept: Seq[Accept]): ToResponse.Aux[A, CTH :+: CTT] =
if (accept.exists(_.matches[CTH])) h.asInstanceOf[ToResponse.Aux[A, CTH :+: CTT]]
else t(accept).asInstanceOf[ToResponse.Aux[A, CTH :+: CTT]]
}

implicit def cnilToNegotiable[A, CTH <: String](implicit
tr: ToResponse.Aux[A, CTH]
): Negotiable[A, CTH :+: CNil] = new Negotiable[A, CTH :+: CNil] {
def apply(accept: Seq[Accept]): ToResponse.Aux[A, CTH :+: CNil] =
tr.asInstanceOf[ToResponse.Aux[A, CTH :+: CNil]]
}

implicit def singleToNegotiable[A, CT <: String](implicit
tr: ToResponse.Aux[A, CT]
): Negotiable[A, CT] = new Negotiable[A, CT] {
def apply(accept: Seq[Accept]): ToResponse.Aux[A, CT] = tr
}
}

implicit def cnilToResponse[CT <: String]: Aux[CNil, CT] =
instance((_, _) => Response(Version.Http10, Status.NotFound))

Expand Down
4 changes: 2 additions & 2 deletions core/src/main/scala/io/finch/ToService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ object ToService {
}

implicit def hlistTS[A, EH <: Endpoint[A], ET <: HList, CTH, CTT <: HList](implicit
ntrA: NegotiateToResponse[A, CTH],
ntrE: NegotiateToResponse[Exception, CTH],
ntrA: ToResponse.Negotiable[A, CTH],
ntrE: ToResponse.Negotiable[Exception, CTH],
tsT: ToService[ET, CTT]
): ToService[Endpoint[A] :: ET, CTH :: CTT] = new ToService[Endpoint[A] :: ET, CTH :: CTT] {
def apply(
Expand Down
86 changes: 85 additions & 1 deletion core/src/test/scala/io/finch/EndToEndSpec.scala
Original file line number Diff line number Diff line change
@@ -1,16 +1,37 @@
package io.finch

import com.twitter.finagle.Service
import com.twitter.finagle.http.{Request, Response, Status}
import com.twitter.finagle.http.{Fields, Request, Response, Status}
import com.twitter.io.Buf
import com.twitter.util.Await
import io.finch.data.Foo
import io.finch.syntax._
import shapeless._

class EndToEndSpec extends FinchSpec {

behavior of "Finch"

type AllContentTypes = Application.Json :+: Application.AtomXml :+: Application.Csv :+:
Application.Javascript :+: Application.OctetStream :+: Application.RssXml :+:
Application.WwwFormUrlencoded :+: Application.Xml :+: Text.Plain :+: Text.Html :+: Text.EventStream :+: CNil

private implicit def encodeHNil[CT <: String]: Encode.Aux[HNil, CT] = Encode.instance((_, _) => Buf.Utf8("hnil"))

private val allContentTypes = Seq(
"application/json",
"application/atom+xml",
"application/csv",
"application/javascript",
"application/octet-stream",
"application/rss+xml",
"application/x-www-form-urlencoded",
"application/xml",
"text/plain",
"text/html",
"text/event-stream"
)

it should "convert coproduct Endpoints into Services" in {
implicit val encodeException: Encode.Text[Exception] =
Encode.text((_, cs) => Buf.ByteArray.Owned("ERR!".getBytes(cs.name)))
Expand Down Expand Up @@ -47,4 +68,67 @@ class EndToEndSpec extends FinchSpec {
rep.contentString shouldBe "bar"
rep.status shouldBe Status.Created
}

it should "ignore Accept header when negotiation is not enabled" in {
check { (req: Request) =>
val s = Bootstrap.serve[AllContentTypes](*).toService
val rep = Await.result(s(req))

rep.contentType === Some("text/event-stream")
}
}

it should "ignore Accept header when single type is used for serve" in {
check { (req: Request) =>
val s = Bootstrap.serve[Text.Plain](*).configure(negotiateContentType = true).toService
val rep = Await.result(s(req))

rep.contentType === Some("text/plain")
}
}

it should "respect Accept header when coproduct type is used for serve" in {
check { (req: Request) =>
val s = Bootstrap.serve[AllContentTypes](*).configure(negotiateContentType = true).toService
val rep = Await.result(s(req))

rep.contentType === req.accept.headOption
}
}

it should "ignore order of values in Accept header and use first appropriate encoder in coproduct" in {
check { (req: Request, accept: Accept) =>
val a = s"${accept.primary}/${accept.sub}"
req.accept = a +: req.accept

val s = Bootstrap.serve[AllContentTypes](*).configure(negotiateContentType = true).toService
val rep = Await.result(s(req))

val first = allContentTypes.collectFirst {
case ct if req.accept.contains(ct) => ct
}

rep.contentType === first
}
}

it should "select last encoder when Accept header is missing/empty" in {
check { (req: Request) =>
req.headerMap.remove(Fields.Accept)
val s = Bootstrap.serve[AllContentTypes](*).configure(negotiateContentType = true).toService
val rep = Await.result(s(req))

rep.contentType === Some("text/event-stream")
}
}

it should "select last encoder when Accept header value doesn't match any existing encoder" in {
check { (req: Request, accept: Accept) =>
req.accept = s"${accept.primary}/foo"
val s = Bootstrap.serve[AllContentTypes](*).configure(negotiateContentType = true).toService
val rep = Await.result(s(req))

rep.contentType === Some("text/event-stream")
}
}
}
93 changes: 0 additions & 93 deletions core/src/test/scala/io/finch/NegotiateToResponseSpec.scala

This file was deleted.

0 comments on commit 8984fbd

Please sign in to comment.