Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* 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)
}