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

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

package io.finch

import cats.syntax.all._
import cats.{Applicative, MonadThrow}
import com.twitter.finagle.http.{Method, Response, Status, Version}
import io.finch.internal.currentTime
import shapeless._

import scala.annotation.implicitNotFound

/** Compiles a given list of [[Endpoint]]s and their content-types into single [[Endpoint.Compiled]].
  *
  * Guarantees to:
  *
  *   - handle Finch's own errors (i.e., [[Error]] and [[Error]]) as 400s
  *   - copy requests's HTTP version onto a response
  *   - respond with 404 when an endpoint is not matched
  *   - respond with 405 when an endpoint is not matched because method wasn't allowed (serve back an `Allow` header)
  *   - include the date header on each response (unless disabled)
  *   - include the server header on each response (unless disabled)
  */
@implicitNotFound("""An Endpoint you're trying to compile is missing one or more encoders.

  Make sure each endpoint in ${ES}, ${CTS} 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
""")
trait Compile[F[_], ES <: HList, CTS <: HList] {
  def apply(endpoints: ES, options: Compile.Options, context: Compile.Context): Endpoint.Compiled[F]
}

object Compile {

  /** HTTP options propagated from [[Bootstrap]]. */
  final case class Options(
      includeDateHeader: Boolean,
      includeServerHeader: Boolean,
      enableMethodNotAllowed: Boolean,
      enableUnsupportedMediaType: Boolean,
      enableNotAcceptable: Boolean
  )

  /** HTTP context propagated between endpoints.
    *
    *   - `wouldAllow`: when non-empty, indicates that the incoming method wasn't allowed/matched
    */
  final case class Context(wouldAllow: List[Method] = Nil)

  private[this] val respond400: PartialFunction[Throwable, Output[Nothing]] = {
    case e: io.finch.Error   => Output.failure(e, Status.BadRequest)
    case es: io.finch.Errors => Output.failure(es, Status.BadRequest)
  }

  private[this] val respond415: PartialFunction[Throwable, Output[Nothing]] = {
    case e: io.finch.Error if e.getCause eq Decode.UnsupportedMediaTypeException =>
      Output.failure(e, Status.UnsupportedMediaType)
  }

  private def conformHttp(rep: Response, version: Version, opts: Options): Response = {
    rep.version = version
    if (opts.includeDateHeader) rep.headerMap.setUnsafe("Date", currentTime())
    if (opts.includeServerHeader) rep.headerMap.setUnsafe("Server", "Finch")
    rep
  }

  implicit def hnilTS[F[_]](implicit F: Applicative[F]): Compile[F, HNil, HNil] = (_, opts, ctx) =>
    Endpoint.Compiled { req =>
      val notAllowed = opts.enableMethodNotAllowed && ctx.wouldAllow.nonEmpty
      val rep = Response(if (notAllowed) Status.MethodNotAllowed else Status.NotFound)
      if (notAllowed) rep.allow = ctx.wouldAllow
      F.pure((Trace.empty, Right(conformHttp(rep, req.version, opts))))
    }

  implicit def hlistTS[F[_]: MonadThrow, A, ET <: HList, CTH, CTT <: HList](implicit
      negotiable: ToResponse.Negotiable[F, A, CTH],
      rest: Compile[F, ET, CTT],
      isNegotiable: CTH <:< Coproduct = null
  ): Compile[F, Endpoint[F, A] :: ET, CTH :: CTT] = { case (e :: es, opts, ctx) =>
    val endpoint = e.handle(if (opts.enableUnsupportedMediaType) respond415 orElse respond400 else respond400)
    Endpoint.Compiled { req =>
      endpoint(Input.fromRequest(req)) match {
        case EndpointResult.Matched(rem, trc, out) if rem.route.isEmpty =>
          val negotiate = isNegotiable != null || (opts.enableNotAcceptable && req.accept.nonEmpty)
          val negotiated = negotiable(if (negotiate) req.accept.map(Accept.fromString).toList else Nil)
          val acceptable = !negotiate || negotiated.acceptable || !opts.enableNotAcceptable
          val rep = if (acceptable) out.flatMap(_.toResponse(negotiated)) else Response(Status.NotAcceptable).pure[F]
          rep.map(conformHttp(_, req.version, opts)).attempt.map((trc, _))
        case EndpointResult.NotMatched.MethodNotAllowed(allowed) =>
          rest(es, opts, ctx.copy(wouldAllow = ctx.wouldAllow ++ allowed))(req)
        case _ =>
          rest(es, opts, ctx)(req)
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy