Skip to content

Commit

Permalink
Merge pull request finagle#610 from finagle/vk/encode-with-charset
Browse files Browse the repository at this point in the history
Encoding with charset
  • Loading branch information
vkostyukov authored Jul 13, 2016
2 parents 7a86f3d + 6c723cc commit 9040dbe
Show file tree
Hide file tree
Showing 17 changed files with 177 additions and 117 deletions.
6 changes: 3 additions & 3 deletions argonaut/src/main/scala/io/finch/argonaut/Encoders.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package io.finch.argonaut

import argonaut.{EncodeJson, PrettyParams}
import com.twitter.io.Buf
import io.finch.Encode
import io.finch.internal.BufText

trait Encoders {

Expand All @@ -11,6 +11,6 @@ trait Encoders {
/**
* Maps Argonaut's [[EncodeJson]] to Finch's [[Encode]].
*/
implicit def encodeArgonaut[A](implicit e: EncodeJson[A]): Encode.ApplicationJson[A] =
Encode.json(a => Buf.Utf8(printer.pretty(e.encode(a))))
implicit def encodeArgonaut[A](implicit e: EncodeJson[A]): Encode.Json[A] =
Encode.json((a, cs) => BufText(printer.pretty(e.encode(a)), cs))
}
6 changes: 3 additions & 3 deletions circe/src/main/scala/io/finch/circe/Encoders.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package io.finch.circe

import com.twitter.io.Buf
import io.circe.{Encoder, Json}
import io.finch.Encode
import io.finch.internal.BufText

trait Encoders {

Expand All @@ -11,6 +11,6 @@ trait Encoders {
/**
* Maps Circe's [[Encoder]] to Finch's [[Encode]].
*/
implicit def encodeCirce[A](implicit e: Encoder[A]): Encode.ApplicationJson[A] =
Encode.json(a => Buf.Utf8(print(e(a))))
implicit def encodeCirce[A](implicit e: Encoder[A]): Encode.Json[A] =
Encode.json((a, cs) => BufText(print(e(a)), cs))
}
49 changes: 28 additions & 21 deletions core/src/main/scala/io/finch/Encode.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,41 @@ package io.finch
import cats.Show
import cats.data.Xor
import com.twitter.io.Buf
import io.finch.internal.BufText
import java.nio.charset.Charset
import shapeless.Witness

/**
* An abstraction that is responsible for encoding the value of type `A`.
* An abstraction that is responsible for encoding the value of type `A` into a `Buf` with a given
* charset.
*/
trait Encode[A] {
type ContentType <: String

def apply(a: A): Buf
def apply(a: A, cs: Charset): Buf
}

trait LowPriorityEncodeInstances {

type Aux[A, CT <: String] = Encode[A] { type ContentType = CT }

type ApplicationJson[A] = Aux[A, Application.Json]
type TextPlain[A] = Aux[A, Text.Plain]
type Json[A] = Aux[A, Application.Json]
type Text[A] = Aux[A, Text.Plain]

def instance[A, CT <: String](fn: A => Buf): Aux[A, CT] =
def instance[A, CT <: String](fn: (A, Charset) => Buf): Aux[A, CT] =
new Encode[A] {
type ContentType = CT
def apply(a: A): Buf = fn(a)
def apply(a: A, cs: Charset): Buf = fn(a, cs)
}

def json[A](fn: A => Buf): ApplicationJson[A] =
instance[A, Witness.`"application/json"`.T](fn)
def json[A](fn: (A, Charset) => Buf): Json[A] =
instance[A, Application.Json](fn)

def text[A](fn: A => Buf): TextPlain[A] =
instance[A, Witness.`"text/plain"`.T](fn)
def text[A](fn: (A, Charset) => Buf): Text[A] =
instance[A, Text.Plain](fn)

implicit def encodeShow[A](implicit s: Show[A]): TextPlain[A] =
text(a => Buf.Utf8(s.show(a)))
implicit def encodeShow[A](implicit s: Show[A]): Text[A] =
text((a, cs) => BufText(s.show(a), cs))
}

object Encode extends LowPriorityEncodeInstances {
Expand All @@ -48,22 +51,26 @@ object Encode extends LowPriorityEncodeInstances {
@inline def apply[A]: Implicitly[A] = new Implicitly[A]

implicit def encodeUnit[CT <: String]: Aux[Unit, CT] =
instance(_ => Buf.Empty)
instance((_, _) => Buf.Empty)

implicit def encodeBuf[CT <: String]: Aux[Buf, CT] =
instance(identity)
instance((buf, _) => buf)

implicit val encodeExceptionAsTextPlain: TextPlain[Exception] =
text(e => Buf.Utf8(Option(e.getMessage).getOrElse("")))
implicit val encodeExceptionAsTextPlain: Text[Exception] = text(
(e, cs) => BufText(Option(e.getMessage).getOrElse(""), cs)
)

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

implicit val encodeString: TextPlain[String] =
text(Buf.Utf8.apply)
implicit val encodeString: Text[String] =
text((s, cs) => BufText(s, cs))

implicit def encodeXor[A, B, CT <: String](implicit
ae: Encode.Aux[A, CT],
be: Encode.Aux[B, CT]
): Encode.Aux[A Xor B, CT] = instance[A Xor B, CT](xor => xor.fold(ae.apply, be.apply))
): Encode.Aux[A Xor B, CT] = instance[A Xor B, CT](
(xor, cs) => xor.fold(a => ae(a, cs), b => be(b, cs))
)
}
12 changes: 9 additions & 3 deletions core/src/main/scala/io/finch/Endpoint.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.twitter.finagle.http.{Cookie, Request, Response, Status}
import com.twitter.util.{Future, Return, Throw, Try}
import io.catbird.util.Rerunnable
import io.finch.internal._
import java.nio.charset.Charset
import shapeless._
import shapeless.ops.adjoin.Adjoin
import shapeless.ops.hlist.Tupler
Expand Down Expand Up @@ -259,19 +260,24 @@ trait Endpoint[A] { self =>
final def withCookie(cookie: Cookie): Endpoint[A] =
withOutput(o => o.withCookie(cookie))

final def withCharset(charset: Charset): Endpoint[A] =
withOutput(o => o.withCharset(charset))

/**
* Converts this endpoint to a Finagle service `Request => Future[Response]` that serves JSON.
*/
final def toService(implicit
tro: ToResponse.Aux[Output[A], Witness.`"application/json"`.T]
): Service[Request, Response] = toServiceAs[Witness.`"application/json"`.T]
tr: ToResponse.Aux[A, Application.Json],
tre: ToResponse.Aux[Exception, Application.Json]
): Service[Request, Response] = toServiceAs[Application.Json]

/**
* Converts this endpoint to a Finagle service `Request => Future[Response]` that serves custom
* content-type `CT`.
*/
final def toServiceAs[CT <: String](implicit
tro: ToResponse.Aux[Output[A], CT]
tr: ToResponse.Aux[A, CT],
tre: ToResponse.Aux[Exception, CT]
): Service[Request, Response] = new Service[Request, Response] {

private[this] val basicEndpointHandler: PartialFunction[Throwable, Output[Nothing]] = {
Expand Down
31 changes: 28 additions & 3 deletions core/src/main/scala/io/finch/Output.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ 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 io.finch.internal.ToResponse
import java.nio.charset.Charset

/**
* An output of [[Endpoint]].
Expand All @@ -28,6 +30,11 @@ sealed trait Output[+A] { self =>
*/
def cookies: Seq[Cookie] = meta.cookies

/**
* The charset of this [[Output]].
*/
def charset: Option[Charset] = meta.charset

/**
* Returns the payload value of this [[Output]] or throws an exception.
*/
Expand Down Expand Up @@ -65,7 +72,13 @@ sealed trait Output[+A] { self =>
}

/**
* Overrides the status code of this [[Output]].
* Overrides `charset` of this [[Output]].
*/
def withCharset(charset: Charset): Output[A] =
withMeta(meta.copy(charset = Some(charset)))

/**
* Overrides the `status` code of this [[Output]].
*/
def withStatus(status: Status): Output[A] =
withMeta(meta.copy(status = status))
Expand All @@ -91,6 +104,7 @@ object Output {
*/
private[finch] case class Meta(
status: Status = Status.Ok,
charset: Option[Charset] = Option.empty,
headers: Map[String, String] = Map.empty[String, String],
cookies: Seq[Cookie] = Seq.empty[Cookie]
)
Expand Down Expand Up @@ -167,11 +181,22 @@ object Output {
* Converts this [[Output]] to the HTTP response of the given `version`.
*/
def toResponse[CT <: String](version: Version = Version.Http11)(implicit
tro: ToResponse.Aux[Output[A], CT]
tr: ToResponse.Aux[A, CT],
tre: ToResponse.Aux[Exception, CT]
): Response = {
val rep = tro(o)
val rep = o match {
case Output.Payload(v, m) => tr(v, m.charset.getOrElse(Charsets.Utf8))
case Output.Failure(x, m) => tre(x, m.charset.getOrElse(Charsets.Utf8))
case Output.Empty(_) => Response()
}

rep.status = o.status
rep.version = version

o.headers.foreach { case (k, v) => rep.headerMap.set(k, v) }
o.cookies.foreach(rep.cookies.add)
o.charset.foreach(c => rep.charset = c.displayName.toLowerCase)

rep
}
}
Expand Down
51 changes: 19 additions & 32 deletions core/src/main/scala/io/finch/internal/ToResponse.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.finch.internal

import java.nio.charset.Charset

import com.twitter.concurrent.AsyncStream
import com.twitter.finagle.http.{Response, Status, Version}
import com.twitter.io.Buf
Expand All @@ -12,45 +14,47 @@ import shapeless._
trait ToResponse[A] {
type ContentType <: String

def apply(a: A): Response
def apply(a: A, cs: Charset): Response
}

trait LowPriorityToResponseInstances {
type Aux[A, CT] = ToResponse[A] { type ContentType = CT }

def instance[A, CT <: String](fn: A => Response): Aux[A, CT] = new ToResponse[A] {
def instance[A, CT <: String](fn: (A, Charset) => Response): Aux[A, CT] = new ToResponse[A] {
type ContentType = CT
def apply(a: A): Response = fn(a)
def apply(a: A, cs: Charset): Response = fn(a, cs)
}

private[this] def asyncStreamResponseBuilder[A, CT <: String](writer: A => Buf)(implicit
private[this] def asyncResponseBuilder[A, CT <: String](writer: (A, Charset) => Buf)(implicit
w: Witness.Aux[CT]
): Aux[AsyncStream[A], CT] = instance { as =>
): Aux[AsyncStream[A], CT] = instance { (as, cs) =>
val rep = Response()
rep.setChunked(true)

val writable = rep.writer

as.foreachF(chunk => writable.write(writer(chunk))).ensure(writable.close())
as.foreachF(chunk => writable.write(writer(chunk, cs))).ensure(writable.close())
rep.contentType = w.value

rep
}

implicit def asyncBufToResponse[CT <: String](implicit
w: Witness.Aux[CT]
): Aux[AsyncStream[Buf], CT] = asyncStreamResponseBuilder(identity)
): Aux[AsyncStream[Buf], CT] = asyncResponseBuilder((a, _) => a)

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

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

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

trait HighPriorityToResponseInstances extends LowPriorityToResponseInstances {
Expand All @@ -69,36 +73,19 @@ trait HighPriorityToResponseInstances extends LowPriorityToResponseInstances {
implicit def valueToResponse[A, CT <: String](implicit
e: Encode.Aux[A, CT],
w: Witness.Aux[CT]
): Aux[A, CT] = instance(a => bufToResponse(e(a), w.value))

implicit def outputToResponse[A, CT <: String](implicit
tr: ToResponse.Aux[A, CT],
e: Encode.Aux[Exception, CT],
w: Witness.Aux[CT]
): Aux[Output[A], CT] = instance { o =>
val rep = o match {
case Output.Payload(v, _) => tr(v)
case Output.Failure(x, _) => bufToResponse(e(x), w.value)
case Output.Empty(_) => Response()
}
rep.status = o.status
o.headers.foreach { case (k, v) => rep.headerMap.set(k, v) }
o.cookies.foreach(rep.cookies.add)

rep
}
): Aux[A, CT] = instance((a, cs) => bufToResponse(e(a, cs), w.value))
}

object ToResponse extends HighPriorityToResponseInstances {

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

implicit def coproductToResponse[H, T <: Coproduct, CT <: String](implicit
trH: ToResponse.Aux[H, CT],
trT: ToResponse.Aux[T, CT]
): Aux[H :+: T, CT] = instance {
case Inl(h) => trH(h)
case Inr(t) => trT(t)
case (Inl(h), cs) => trH(h, cs)
case (Inr(t), cs) => trT(t, cs)
}
}
19 changes: 19 additions & 0 deletions core/src/main/scala/io/finch/internal/package.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package io.finch

import com.twitter.io.{Buf, Charsets}
import java.nio.CharBuffer
import java.nio.charset.Charset

/**
* This package contains an internal-use only type-classes and utilities that power Finch's API.
*
Expand Down Expand Up @@ -85,4 +89,19 @@ package object internal {
if (s.length == 0 || s.length > 32) None
else parseLong(s, Long.MinValue, Long.MaxValue)
}

// TODO: Move to twitter/util
object BufText {
def apply(s: String, cs: Charset): Buf = {
val enc = Charsets.encoder(cs)
val cb = CharBuffer.wrap(s.toCharArray)
Buf.ByteBuffer.Owned(enc.encode(cb))
}

def extract(buf: Buf, cs: Charset): String = {
val dec = Charsets.decoder(cs)
val bb = Buf.ByteBuffer.Owned.extract(buf).asReadOnlyBuffer
dec.decode(bb).toString
}
}
}
Loading

0 comments on commit 9040dbe

Please sign in to comment.