All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.finch.ToResponse.scala Maven / Gradle / Ivy

package io.finch

import cats.{Applicative, Functor}
import com.twitter.finagle.http.{Response, Status, Version}
import shapeless._

import java.nio.charset.Charset
import scala.annotation.implicitNotFound

/** Represents a conversion from `A` to [[com.twitter.finagle.http.Response]]. */
trait ToResponse[F[_], A] {
  type ContentType
  def apply(a: A, cs: Charset): F[Response]
}

trait ToResponseInstances {
  @implicitNotFound("""An Endpoint you're trying to convert into a Finagle service is missing one or more encoders.

  Make sure ${A} is one of the following:

  * A com.twitter.finagle.http.Response
  * A value of a type with an io.finch.Encode instance (with the corresponding content-type)
  * A coproduct made up of some combination of the above

  See https://github.com/finagle/finch/blob/master/docs/src/main/tut/cookbook.md#fixing-the-toservice-compile-error
  """)
  type Aux[F[_], A, CT] = ToResponse[F, A] {
    type ContentType = CT
  }

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

  implicit def responseToResponse[F[_], CT <: String](implicit F: Applicative[F]): Aux[F, Response, CT] =
    instance((rep, _) => F.pure(rep))

  implicit def valueToResponse[F[_], A, CT <: String](implicit
      F: Applicative[F],
      A: Encode.Aux[A, CT],
      CT: Witness.Aux[CT]
  ): Aux[F, A, CT] = instance { (a, cs) =>
    val buf = A(a, cs)
    val rep = Response(Version.Http11, Status.Ok)
    if (!buf.isEmpty) {
      rep.content = buf
      rep.headerMap.setUnsafe("Content-Type", CT.value)
    }

    F.pure(rep)
  }

  implicit def streamToResponse[F[_], S[_[_], _], A, CT <: String](implicit
      F: Functor[F],
      S: EncodeStream.Aux[F, S, A, CT],
      CT: Witness.Aux[CT]
  ): Aux[F, S[F, A], CT] = instance { (a, cs) =>
    F.map(S(a, cs)) { stream =>
      val rep = Response(Version.Http11, Status.Ok, stream)
      rep.headerMap.setUnsafe("Content-Type", CT.value)
      rep.headerMap.setUnsafe("Transfer-Encoding", "chunked")
      rep
    }
  }
}

object ToResponse extends ToResponseInstances {
  implicit def coproductToResponse[F[_], C <: Coproduct, CT](implicit
      fc: FromCoproduct.Aux[F, C, CT]
  ): Aux[F, C, CT] = fc

  final case class Negotiated[F[_], A](
      value: ToResponse[F, A],
      error: ToResponse[F, Exception],
      acceptable: Boolean = true
  )

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

  object Negotiable {
    implicit def coproductToNegotiable[F[_], A, CTH <: String, CTT <: Coproduct](implicit
        value: Aux[F, A, CTH],
        error: Aux[F, Exception, CTH],
        rest: Negotiable[F, A, CTT],
        matcher: Accept.Matcher[CTH]
    ): Negotiable[F, A, CTH :+: CTT] =
      accept => if (matcher(accept)) Negotiated(value, error) else rest(accept)

    implicit def cnilToNegotiable[F[_], A, CTH <: String](implicit
        value: Aux[F, A, CTH],
        error: Aux[F, Exception, CTH],
        matcher: Accept.Matcher[CTH]
    ): Negotiable[F, A, CTH :+: CNil] =
      accept => Negotiated(value, error, matcher(accept))

    implicit def singleToNegotiable[F[_], A, CT <: String](implicit
        value: Aux[F, A, CT],
        error: Aux[F, Exception, CT],
        matcher: Accept.Matcher[CT]
    ): Negotiable[F, A, CT] =
      accept => Negotiated(value, error, matcher(accept))
  }

  trait FromCoproduct[F[_], C <: Coproduct] extends ToResponse[F, C]
  object FromCoproduct {
    type Aux[F[_], C <: Coproduct, CT] = FromCoproduct[F, C] {
      type ContentType = CT
    }

    def instance[F[_], C <: Coproduct, CT](fn: (C, Charset) => F[Response]): Aux[F, C, CT] =
      new FromCoproduct[F, C] {
        type ContentType = CT
        def apply(c: C, cs: Charset): F[Response] = fn(c, cs)
      }

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

    implicit def cconsToResponse[F[_], L, R <: Coproduct, CT](implicit
        tr: ToResponse.Aux[F, L, CT],
        fc: Aux[F, R, CT]
    ): Aux[F, L :+: R, CT] = instance {
      case (Inl(l), cs) => tr(l, cs)
      case (Inr(r), cs) => fc(r, cs)
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy