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

com.pagerduty.funhttpclient.Http.scala Maven / Gradle / Ivy

There is a newer version: 2.1.0
Show newest version
/*
 * Copyright 2015 PagerDuty, Inc.
 *
 * Author: Jesse Haber-Kucharsky 
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.pagerduty.funhttpclient

import com.twitter.util.{Duration, Stopwatch, Time}
import scalaz.{Failure => _, Success => _, _}, Scalaz._
import spray.http._

import scala.concurrent._
import scala.util._

import ExecutionContext.Implicits.global

/**
 * A computation that involves remote requests over HTTP.
 *
 * [[Http]] computations are asynchronous, log HTTP requests that are made, and have explicit error
 * handling (without exceptions).
 *
 * They have a simple functional interface consisting of:
 *
 *   - [[flatMap]]
 *   - [[map]]
 *   - [[andThen]]
 *
 * as well as additional functions for error handling by-value.
 *
 * [[Http]] computations can have one of two outcomes: an error of type `E` or a result of type `A`.
 *
 * Errors short-circuit computations and are threaded to the final result.
 *
 * [[Http]] computations should be converted to Scala futures via [[asFuture]] or
 * [[asFutureWithLog]] for further computation.
 *
 * To those with functional programming experience, an [[Http]] is essentially a monad transformer
 * consisting of an either, a writer, and a Scala [[Future]].
 *
 */
case class Http[E, A] private[funhttpclient] (
    run: EitherT[({ type L[X] = WriterT[Future, Vector[LogEntry], X] })#L, E, A])
  extends HttpInstances
{
  val httpOps: HttpOps[E] =
    HttpOps[E]

  import httpOps._

  private[funhttpclient] def unwrap: Future[(RequestLog, \/[E, A])] =
    run.run.run

  /**
   * Finalize the HTTP computation by getting the underlying future value.
   *
   * The result of the computation is the log of HTTP requests made as well as the computation
   * (or the error that occurred).
   *
   * ===Example===
   *
   * {{{
   *   import scala.concurrent._
   *   import scala.concurrent.duration._
   *
   *   val x = HttpOps[String].unit(3)
   *   val fut = x.map(_ * 2).asFutureWithLog
   *
   *   scala> Await.result(fut, Duration.Inf)
   *   res0: (Vector[LogEntry], \/[String,Int]) = (Vector(),\/-(6))
   * }}}
   */
  def asFutureWithLog: Future[(RequestLog, \/[E, A])] =
    unwrap

  /**
   * Like [[asFutureWithLog]], but without the request log.
   */
  def asFuture: Future[\/[E, A]] =
    asFutureWithLog.map(_._2)

  /**
   * Like [[asFuture]], but with the error computation as an [[Either]] type.
   *
   * @see [[asFuture]]
   */
  def asFutureEither: Future[Either[E, A]] =
    asFuture.map(_.toEither)

  /**
   * Finalize the HTTP computation without caring about the error type.
   *
   * Any error value is converted to [[None]].
   *
   * @see [[asFuture]], [[asFutureWithLog]]
   */
  def asFutureOption: Future[Option[A]] =
    asFuture.map(_.toOption)

  /**
   * ===Example===
   *
   * {{{
   *   scala> :t HttpOps[String].unit(3) flatMap { x => HttpOps[String].unit(x.toDouble) }
   *   com.pagerduty.sprayfunclient.Http[String,Double]
   * }}}
   */
  def flatMap[B](f: A => Http[E, B]): Http[E, B] =
    monad.bind(this)(f)

  /**
   * Map over the successful result of the computation.
   *
   * ===Example===
   *
   * {{{
   *   scala> :t HttpOps[String].unit(3).map(_.toDouble)
   *   com.pagerduty.sprayfunclient.Http[String,Double]
   * }}}
   */
  def map[B](f: A => B): Http[E, B] =
    flatMap { a => async(f(a)) }

  /**
   * Sequence two computations, disregarding the result of the first.
   *
   * ===Example===
   *
   * {{{
   *   val say = HttpOps[String].unit(println("Hello!"))
   *   val x = say andThen HttpOps[String].unit(3)
   *
   *   scala> :t x
   *   com.pagerduty.sprayfunclient.Http[String, Int]
   * }}}
   */
  def andThen[B](fb: Http[E, B]): Http[E, B] =
    flatMap { _ => fb }

  /**
   * Recover from errors.
   *
   * ===Example===
   *
   * {{{
   *   val badComputation = HttpOps[String].raiseError[Int]("error1")
   *
   *   for {
   *     x <- HttpOps[String].unit(5)
   *
   *     y <- badComputation handleError {
   *       case "error1" => HttpOps[String].async(3)
   *     }
   *   } yield x + y
   * }}}
   */
  def handleError(f: E => Http[E, A]): Http[E, A] =
    monadError.handleError(this)(f)

  /**
   * Map over the result of a failed computation.
   *
   * ===Example==
   *
   * {{{
   *   case class BadThing(message: String)
   *
   *   val x = HttpOps[String].async(3)
   *
   *   scala> :t x.mapError(BadThing.apply)
   *   com.pagerduty.sprayfunclient.Http[BadThing, Int]
   * }}}
   */
  def mapError[EE](f: E => EE): Http[EE, A] =
    Http(run.leftMap(f))
}

trait HttpOps[E] extends HttpInstances {

  /**
   * Lift a value into an HTTP computation.
   *
   * The value is strict (computed immediately).
   *
   * ===Example===
   *
   * {{{
   *   val x = HttpOps[String].unit(3.2)
   * }}}
   */
  def unit[A](a: A): Http[E, A] = {
    val w: W[\/[E, A]] = WriterT(Future.successful((Vector(), a.right)))
    Http(EitherT(w))
  }

  /**
   * Lift a value asynchronously into a HTTP computation.
   *
   * The result will be computed concurrently according to [[ExecutionContext.global]].
   *
   * {{{
   *   lazy val bigExpensiveComputation: Int = ???
   *
   *   val x: Http[String, Int] = HttpOps[String].async(bigExpensiveComputation)
   * }}}
   */
  def async[A](a: => A): Http[E, A] =
    Monad[({ type L[X] = H[E, X] })#L].point(a)

  /**
   * Add a HTTP request and it's execution time to the request log.
   *
   * This log is accessible when the computation is "run" via [[Http.asFutureWithLog]].
   *
   * @note [[request]] automatically invokes [[trace]], so it is '''not''' necessary to
   *      invoke it manually when making HTTP requests.
   */
  def trace(entry: LogEntry): Http[E, Unit] = {
    val w: W[\/[E, Unit]] = ().right.pure[W] :++> Vector(entry)
    Http(EitherT(w))
  }

  /**
   * Fail the computation with a specific error.
   *
   * The error will short-circuit any future computations.
   *
   * Errors can be manipulated via [[Http.mapError]] or recovered from via [[Http.handleError]].
   *
   * ===Example===
   *
   * {{{
   *   def sqrt(x: Double): Http[String, Double] = {
   *     if (x < 0)
   *       HttpOps[String].raiseError("negative number")
   *     else
   *       HttpOps[String].unit(math.sqrt(x))
   *   }
   *
   *   scala> sqrt(4.0)
   *   res0: com.pagerduty.sprayfunclient.Http[String, Double] = // ...
   *
   *   scala> sqrt(-4)
   *   res1: com.pagerduty.sprayfunclient.Http[String, Double] = // ...
   * }}}
   *
   */
  def raiseError[A](e: E): Http[E, A] =
    monadError.raiseError(e)

  /**
   * Wrap an arbitrary asynchronous computation, and selectively recover from them.
   *
   * If a particular exception isn't handled, then it is propagated up via [[Future.failed]].
   *
   * ===Example===
   *
   * {{{
   *   import scala.concurrent.ExecutionContext.Implicits.global
   *
   *   val x: Http[String, Int] = HttpOps[String].liftFutureHandle(Future("abc".toInt)) {
   *     case _: NumberFormatException => HttpOps[String].raiseError("Bad number")
   *   }
   *
   *   scala> x
   *   res0: com.pagerduty.sprayfunclient.Http[String, Int] = // ...
   * }}}
   *
   * @see [[HttpOps.liftFuture]]
   */
  def liftFutureHandle[A](fut: Future[A])(f: PartialFunction[Throwable, Http[E, A]])
  : Http[E, A] =
  {
    val promise: Promise[Http[E, A]] = Promise()

    fut.onComplete {
      case Success(v) => promise.success(unit(v))
      case Failure(error: Throwable) =>
        if (f.isDefinedAt(error))
          promise.success(f(error))
        else
          promise.failure(error)
    }

    fromFuture(promise.future.map(_.unwrap).join)
  }

  /**
   * Wrap an asynchronous computation without handling exceptions.
   *
   * '''Any''' exception thrown by the original future will propagate up via [[Future.failed]].
   *
   * @see [[HttpOps.liftFutureHandle]]
   */
  def liftFuture[A](fut: Future[A]): Http[E, A] =
    liftFutureHandle(fut)(PartialFunction.empty: PartialFunction[Throwable, Http[E, A]])

  /**
   * Map over the result of two independent HTTP computations.
   *
   * ===Example===
   *
   * {{{
   *   val x: Http[String, Int] = HttpOps[String].unit(3)
   *   val y: Http[String, Double] = HttpOps[String].unit(4.2)
   *
   *   scala> HttpOps[String].map2(x, y) { _.toInt + _ }
   *   res0: com.pagerduty.sprayfunclient.Http[String, Double] = // ...
   * }}}
   */
  def map2[A, B, C](ha: Http[E, A], hb: Http[E, B])(f: (A, B) => C): Http[E, C] =
    ha flatMap { a =>
      hb flatMap { b =>
        unit(f(a, b))
      }
    }

  /**
   * Execute multiple HTTP computations.
   *
   * Asynchronous computations started via [[async]] will execute concurrently.
   *
   * The computation fails if any one of the computations fail.
   *
   * ===Example===
   *
   * {{{
   *   val x = HttpOps[String].async("First big computation")
   *   val y = HttpOps[String].async("Second big computation")
   *
   *   scala> HttpOps[String].sequence(Seq(x, y))
   *   res0: com.pagerduty.sprayfunclient.Http[String, Seq[String]] = // ...
   * }}}
   */
  def sequence[A](hs: Seq[Http[E, A]]): Http[E, Seq[A]] =
    hs.foldRight(unit(Seq.empty): Http[E, Seq[A]]) { case (h, accum) => map2(h, accum)(_ +: _) }

  private def fromFuture[A](fut: Future[(RequestLog, \/[E, A])]): Http[E, A] = {
    val w: W[\/[E, A]] = WriterT(fut)
    Http(EitherT(w))
  }
}

object HttpOps extends HttpInstances {

  /**
   * Create a new module for [[HttpOps]] specialized to a specific error type.
   *
   * Instead of
   *
   * {{{
   *   val x = HttpOps[String].unit(3)
   * }}}
   *
   * one can say
   *
   * {{{
   *   val httpOps = HttpOps[String]
   *   import httpOps._
   *
   *   val x = unit(3)
   * }}}
   */
  def apply[E]: HttpOps[E] =
    new HttpOps[E] {}

  /**
   * Execute an HTTP request.
   *
   * Any requests made are added to the request log automatically.
   *
   * Any HTTP errors (time-outs, connection problems, malformed responses) will result in the
   * computation failing with [[NetworkError]].
   */
  def request(r: HttpRequest)(client: HttpDriver): Http[NetworkError, HttpResponse] = {
    val ops = HttpOps[NetworkError]

    val now = Time.now
    val elapsed = Stopwatch.start()

    for {
      response <- ops.liftFutureHandle(client.execute(r)) {
        case error: Throwable => ops.raiseError[HttpResponse](NetworkError(error))
      } handleError {
        case e: NetworkError => ops.trace(LogEntry.failed(r, now)) andThen ops.raiseError(e)
      }

      _ <- ops.trace(LogEntry(r, now, elapsed()))
    } yield response
  }
}

/**
 * Scalaz instances.
 *
 * Here be dragons.
 *
 * These monad instances exist to make the implementation of this client easier, but they are not
 * exposed by the interface and users of this library don't have to worry about them.
 */
trait HttpInstances {
  type RequestLog = Vector[LogEntry]

  type W[A] = WriterT[Future, RequestLog, A]
  type D[E, A] = EitherT[W, E, A]
  type H[E, A] = Http[E, A]

  implicit def monad[E] =
    new MonadInstance[E] {}

  implicit def monadError[E] =
    new MonadErrorInstance[E] {}

  trait MonadInstance[E] extends Monad[({ type L[X] = H[E, X] })#L] {
    type D0[A] = D[E, A]
    type H0[A] = H[E, A]

    implicit val writerTMonad = WriterT.writerTMonad[Future, RequestLog]
    implicit val eitherTMonad = EitherT.eitherTMonad[D0, E]

    override def point[A](a: => A): Http[E, A] =
      Http(a.point[D0])

    override def bind[A, B](fa: H0[A])(f: A => H0[B]): H0[B] =
      Http(fa.run.flatMap { a => f(a).run })
  }

  trait MonadErrorInstance[E] extends MonadError[H, E] with MonadInstance[E] {
    override def raiseError[A](e: E): Http[E, A] =
      Http(e.raiseError[D, A])

    override def handleError[A](fa: Http[E, A])(f: E => Http[E, A]): Http[E, A] =
      Http(MonadError[D, E].handleError(fa.run) { e => f(e).run })
  }
}

case class NetworkError(inner: Throwable)

/**
 * Every HTTP request made is logged with the actual request, the time it was initiated, and it's
 * duration.
 */
case class LogEntry private (
    request: HttpRequest,
    initiationTime: Time,
    completed: Boolean,
    duration: Option[Duration])

object LogEntry {
  def apply(r: HttpRequest, initiationTime: Time, duration: Duration): LogEntry =
    LogEntry(r, initiationTime, completed = true, Some(duration))

  def failed(r: HttpRequest, initiationTime: Time): LogEntry =
    LogEntry(r, initiationTime, completed = false, None)
}





© 2015 - 2024 Weber Informatics LLC | Privacy Policy