io.finch.Output.scala Maven / Gradle / Ivy
package io.finch
import cats.{Applicative, Eq}
import com.twitter.finagle.http.{Cookie, Response, Status}
import java.nio.charset.{Charset, StandardCharsets}
/** An output of [[Endpoint]].
*/
sealed trait Output[+A] { self =>
/** The status code of this [[Output]].
*/
def status: Status
/** The header map of this [[Output]].
*/
def headers: Map[String, String]
/** The cookie list of this [[Output]].
*/
def cookies: List[Cookie]
/** The charset of this [[Output]].
*/
def charset: Option[Charset]
/** Returns the payload value of this [[Output]] or throws an exception.
*/
def value: A
final def map[B](fn: A => B): Output[B] = this match {
case p: Output.Payload[A] => p.withValue(fn(p.value))
case f: Output.Failure => f
case e: Output.Empty => e
}
final def flatMap[B](fn: A => Output[B]): Output[B] = this match {
case p: Output.Payload[A] => fn(p.value).withCookies(p.cookies).withHeaders(p.headers)
case f: Output.Failure => f
case e: Output.Empty => e
}
final def traverse[F[_], B](fn: A => F[B])(implicit F: Applicative[F]): F[Output[B]] = this match {
case p: Output.Payload[A] => F.map(fn(p.value))(b => p.withValue(b))
case f: Output.Failure => F.pure(f)
case e: Output.Empty => F.pure(e)
}
final def traverseFlatten[F[_], B](fn: A => F[Output[B]])(implicit F: Applicative[F]): F[Output[B]] = this match {
case p: Output.Payload[A] =>
F.map(fn(p.value))(ob => ob.withHeaders(self.headers).withCookies(self.cookies))
case f: Output.Failure => F.pure(f)
case e: Output.Empty => F.pure(e)
}
/** Overrides `charset` of this [[Output]].
*/
final def withCharset(charset: Charset): Output[A] =
copy(charset = Some(charset))
/** Overrides the `status` code of this [[Output]].
*/
final def withStatus(status: Status): Output[A] =
copy(status = status)
/** Adds given `headers` to this [[Output]].
*/
final def withHeaders(headers: Map[String, String]): Output[A] =
if (headers.isEmpty) this
else copy(headers = self.headers ++ headers)
/** Adds given `cookies` to this [[Output]].
*/
final def withCookies(cookies: List[Cookie]): Output[A] =
if (cookies.isEmpty) this
else copy(cookies = self.cookies ++ cookies)
/** Adds a given `header` to this [[Output]].
*/
final def withHeader(header: (String, String)): Output[A] = withHeaders(Map(header))
/** Adds a given `cookie` to this [[Output]].
*/
final def withCookie(cookie: Cookie): Output[A] = withCookies(List(cookie))
protected def copy(
status: Status = self.status,
charset: Option[Charset] = self.charset,
headers: Map[String, String] = self.headers,
cookies: List[Cookie] = self.cookies
): Output[A]
}
object Output {
/** Creates a successful [[Output]] that wraps a payload `value` with given `status`.
*/
final def payload[A](value: A, status: Status = Status.Ok): Output[A] =
Payload(value, status)
/** Creates a failure [[Output]] that wraps an exception `cause` causing this.
*/
final def failure[A](cause: Exception, status: Status = Status.BadRequest): Output[A] =
Failure(cause, status)
/** Creates an empty [[Output]] of given `status`.
*/
final def empty[A](status: Status): Output[A] = Empty(status)
/** Creates a unit/empty [[Output]] (i.e., `Output[Unit]`) of given `status`.
*/
final def unit(status: Status): Output[Unit] = empty(status)
/** An [[Output]] with `None` as a payload.
*/
val None: Output[Option[Nothing]] = Output.payload(Option.empty[Nothing])
/** An [[Output]] with `HNil` as a payload. */
val HNil: Output[shapeless.HNil] = Output.payload(shapeless.HNil)
/** A successful [[Output]] that captures a payload `value`.
*/
final private[finch] case class Payload[A](
value: A,
status: Status = Status.Ok,
charset: Option[Charset] = Option.empty,
headers: Map[String, String] = Map.empty[String, String],
cookies: List[Cookie] = List.empty[Cookie]
) extends Output[A] { self =>
def withValue[B](value: B): Payload[B] = Payload(value, status, charset, headers, cookies)
protected def copy(status: Status, charset: Option[Charset], headers: Map[String, String], cookies: List[Cookie]): Output[A] =
Payload(value, status, charset, headers, cookies)
}
/** A failure [[Output]] that captures an [[Exception]] explaining why it's not a payload or an empty response.
*/
final private[finch] case class Failure(
cause: Exception,
status: Status = Status.BadRequest,
charset: Option[Charset] = Option.empty,
headers: Map[String, String] = Map.empty[String, String],
cookies: List[Cookie] = List.empty[Cookie]
) extends Output[Nothing] {
def value: Nothing = throw cause
protected def copy(status: Status, charset: Option[Charset], headers: Map[String, String], cookies: List[Cookie]): Output[Nothing] =
Failure(cause, status, charset, headers, cookies)
}
/** An empty [[Output]] that does not capture any payload.
*/
final private[finch] case class Empty(
status: Status,
charset: Option[Charset] = Option.empty,
headers: Map[String, String] = Map.empty[String, String],
cookies: List[Cookie] = List.empty[Cookie]
) extends Output[Nothing] {
def value: Nothing = throw new IllegalStateException("empty output")
protected def copy(status: Status, charset: Option[Charset], headers: Map[String, String], cookies: List[Cookie]): Output[Nothing] =
Empty(status, charset, headers, cookies)
}
implicit def outputEq[A]: Eq[Output[A]] = Eq.fromUniversalEquals
implicit class OutputOps[A](private val output: Output[A]) extends AnyVal {
/** Converts this [[Output]] to the HTTP response of the given `version`. */
def toResponse[F[_]: Applicative, CT](implicit
value: ToResponse.Aux[F, A, CT],
error: ToResponse.Aux[F, Exception, CT]
): F[Response] = toResponse(ToResponse.Negotiated(value, error))
/** Converts this [[Output]] to the HTTP response of the given `version`. */
def toResponse[F[_]](negotiated: ToResponse.Negotiated[F, A])(implicit F: Applicative[F]): F[Response] = {
val response = output match {
case p: Output.Payload[A] => negotiated.value(p.value, p.charset.getOrElse(StandardCharsets.UTF_8))
case f: Output.Failure => negotiated.error(f.cause, f.charset.getOrElse(StandardCharsets.UTF_8))
case _: Output.Empty => F.pure(Response())
}
F.map(response) { rep =>
rep.status = output.status
output.headers.foreach { case (k, v) => rep.headerMap.set(k, v) }
output.cookies.foreach(rep.cookies.add)
output.charset.foreach { c =>
if (!rep.content.isEmpty || rep.isChunked) {
rep.charset = c.displayName.toLowerCase
}
}
rep
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy