Repository: gvolpe/advanced-http4s
Branch: master
Commit: f6bee1e016cb
Files: 30
Total size: 36.3 KB
Directory structure:
gitextract_r37kbv80/
├── .gitignore
├── README.md
├── build.sbt
├── project/
│ ├── Dependencies.scala
│ └── build.properties
└── src/
├── main/
│ └── scala/
│ └── com/
│ └── github/
│ └── gvolpe/
│ ├── fs2/
│ │ ├── Counter.scala
│ │ ├── Fifo.scala
│ │ ├── Once.scala
│ │ ├── PubSub.scala
│ │ ├── Resources.scala
│ │ └── package.scala
│ └── http4s/
│ ├── StreamUtils.scala
│ ├── client/
│ │ ├── MultipartClient.scala
│ │ └── StreamClient.scala
│ └── server/
│ ├── Module.scala
│ ├── Server.scala
│ ├── endpoints/
│ │ ├── FileHttpEndpoint.scala
│ │ ├── HexNameHttpEndpoint.scala
│ │ ├── JsonXmlHttpEndpoint.scala
│ │ ├── MultipartHttpEndpoint.scala
│ │ ├── TimeoutHttpEndpoint.scala
│ │ ├── auth/
│ │ │ ├── AuthRepository.scala
│ │ │ ├── BasicAuthHttpEndpoint.scala
│ │ │ └── GitHubHttpEndpoint.scala
│ │ └── package.scala
│ └── service/
│ ├── FileService.scala
│ └── GitHubService.scala
└── test/
└── scala/
└── com/
└── github/
└── gvolpe/
└── http4s/
├── IOAssertion.scala
└── server/
└── endpoints/
├── HexNameHttpEndpointSpec.scala
└── JsonXmlHttpEndpointSpec.scala
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
ml
*.ipr
*.iws
*.pyc
*.tm.epoch
*.vim
*/project/boot
*/project/build/target
*/project/project.target.config-classes
*-shim.sbt
*~
.#*
.*.swp
.DS_Store
.cache
.cache
.classpath
.codefellow
.ensime*
.eprj
.history
.idea
.manager
.multi-jvm
.project
.scala_dependencies
.scalastyle
.settings
.tags
.tags_sorted_by_file
.target
.worksheet
Makefile
TAGS
lib_managed
logs
project/boot/*
project/plugins/project
src_managed
target
tm*.lck
tm*.log
tm.out
worker*.log
/bin
t
tags
tq:q
TODO
================================================
FILE: README.md
================================================
advanced-http4s
===============
Code samples of advanced features of [Http4s](http://http4s.org/) in combination with some features of [Fs2](https://functional-streams-for-scala.github.io/fs2/) not often seen.
Streaming end to end
--------------------
- **Server**: Streaming responses end to end, from the `FileService` reading all the directories in your `$HOME` directory to the `FileHttpEndpoint`.
- **StreamClient**: Parsing chunks of the response body produced by the server in a streaming fashion way.
> You'll need two sbt sessions. Run the server in one and after the client in the other to try it out.
Middleware Composition
----------------------
- **GZip**: For compressed responses. Client must set the `Accept Encoding` header to `gzip`.
- **AutoSlash**: To make endpoints work with and without the slash `/` at the end.
> Response compression is verified by `HexNameHttpEndpointSpec`. You can also try it out on Postman or similar.
- **Timeout**: Handling response timeouts with the given middleware.
> The `TimeoutHttpEndpoint` generates a response in a random time to demonstrate the use.
- **NonStreamResponse**: Using the `ChunkAggregator` middleware to wrap the streaming `FileHttpEndpoint` and remove the Chunked Transfer Encoding.
> The endpoint `/v1/nonstream/dirs?depth=3` demonstrates the use case.
Media Type negotiation
----------------------
- **XML** and **Json**: Decoding request body with either of these types for the same endpoint.
> The `JsonXmlHttpEndpoint` demonstrates this use case and it's validated in its spec.
Multipart Form Data
-------------------
- **Server**: The `MultipartHttpEndpoint` is responsible for parsing multipart data with the given multipart decoder.
- **MultipartClient**: Example uploading both text and an image.
> Similar to the streaming example, you'll need to run both Server and MultipartClient to see how it works.
*NOTE: Beware of the creation of `rick.jpg` file in your HOME directory!*
Authentication
--------------
- **Basic Auth**: Using the given middleware as demonstrated by the `BasicAuthHttpEndpoint`.
- **OAuth 2**: Using GitHub as demonstrated by the `GitHubHttpEndpoint`.
-----------------------------------------------------------------------------
fs2 examples
============
In the fs2 package you'll find some practical examples of the few things it's possible to build with this powerful streaming library. This might serve as a starting point, your creativity will do the rest.
fs2.async package
------------------
Apart from the use of the three core types `Stream[F, O]`, `Pipe[F, I, O]` and `Sink[F, I]` you'll find examples of use of the following types:
- `Topic[F, A]`
- `Signal[F, A]`
- `Queue[F, A]`
- `Ref[F, A]`
- `Promise[F, A]`
- `Semaphore[F]`
In addition to the use of some other functions useful in Parallel and Concurrent scenarios.
================================================
FILE: build.sbt
================================================
import Dependencies._
lazy val root = (project in file(".")).
settings(
inThisBuild(List(
organization := "com.github.gvolpe",
scalaVersion := "2.12.4",
version := "0.1.0-SNAPSHOT",
scalacOptions := Seq(
"-deprecation",
"-encoding",
"UTF-8",
"-feature",
"-language:existentials",
"-language:higherKinds",
"-Ypartial-unification"
)
)),
name := "Advanced Http4s",
libraryDependencies ++= Seq(
Libraries.catsEffect,
Libraries.monix,
Libraries.fs2Core,
Libraries.http4sServer,
Libraries.http4sClient,
Libraries.http4sDsl,
Libraries.http4sCirce,
Libraries.http4sXml,
Libraries.circeCore,
Libraries.circeGeneric,
Libraries.typesafeConfig,
Libraries.logback,
Libraries.scalaTest,
Libraries.scalaCheck
),
addCompilerPlugin("org.spire-math" % "kind-projector" % "0.9.5" cross CrossVersion.binary)
)
================================================
FILE: project/Dependencies.scala
================================================
import sbt._
object Dependencies {
object Versions {
val CatsEffect = "0.8"
val Monix = "3.0.0-M3"
val Fs2 = "0.10.2"
val Http4s = "0.18.1"
val Tsec = "0.0.1-M9"
val Circe = "0.9.1"
val ScalaTest = "3.0.4"
val ScalaCheck = "1.13.4"
val Logback = "1.2.1"
val TypesafeCfg = "1.3.1"
}
object Libraries {
lazy val catsEffect = "org.typelevel" %% "cats-effect" % Versions.CatsEffect
lazy val monix = "io.monix" %% "monix" % Versions.Monix
lazy val fs2Core = "co.fs2" %% "fs2-core" % Versions.Fs2
lazy val fs2IO = "co.fs2" %% "fs2-io" % Versions.Fs2
lazy val http4sServer = "org.http4s" %% "http4s-blaze-server" % Versions.Http4s
lazy val http4sClient = "org.http4s" %% "http4s-blaze-client" % Versions.Http4s
lazy val http4sDsl = "org.http4s" %% "http4s-dsl" % Versions.Http4s
lazy val http4sCirce = "org.http4s" %% "http4s-circe" % Versions.Http4s
lazy val http4sXml = "org.http4s" %% "http4s-scala-xml" % Versions.Http4s
lazy val tsecJwtMac = "io.github.jmcardon" %% "tsec-jwt-mac" % Versions.Tsec
lazy val circeCore = "io.circe" %% "circe-core" % Versions.Circe
lazy val circeGeneric = "io.circe" %% "circe-generic" % Versions.Circe
lazy val typesafeConfig = "com.typesafe" % "config" % Versions.TypesafeCfg
lazy val logback = "ch.qos.logback" % "logback-classic" % Versions.Logback
lazy val scalaTest = "org.scalatest" %% "scalatest" % Versions.ScalaTest % Test
lazy val scalaCheck = "org.scalacheck" %% "scalacheck" % Versions.ScalaCheck % Test
}
}
================================================
FILE: project/build.properties
================================================
sbt.version=1.1.0
================================================
FILE: src/main/scala/com/github/gvolpe/fs2/Counter.scala
================================================
package com.github.gvolpe.fs2
import cats.effect.{Effect, IO}
import fs2.StreamApp.ExitCode
import fs2.async.Ref
import fs2.{Scheduler, Sink, Stream, StreamApp, async}
import scala.concurrent.ExecutionContext.Implicits.global
object CounterApp extends Counter[IO]
/**
* Concurrent counter that demonstrates the use of [[fs2.async.Ref]].
*
* The workers will concurrently run and modify the value of the Ref so this is one
* possible outcome showing "#worker >> currentCount":
*
* #1 >> 0
* #3 >> 0
* #2 >> 0
* #1 >> 2
* #2 >> 3
* #3 >> 3
* */
class Counter[F[_] : Effect] extends StreamApp[F] {
override def stream(args: List[String], requestShutdown: F[Unit]): fs2.Stream[F, ExitCode] =
Scheduler(corePoolSize = 10).flatMap { implicit S =>
for {
ref <- Stream.eval(async.refOf[F, Int](0))
w1 = new Worker[F](1, ref)
w2 = new Worker[F](2, ref)
w3 = new Worker[F](3, ref)
ec <- Stream(w1.start, w2.start, w3.start).join(3).drain ++ Stream.emit(ExitCode.Success)
} yield ec
}
}
class Worker[F[_]](number: Int, ref: Ref[F, Int])
(implicit F: Effect[F]) {
private val sink: Sink[F, Int] = _.evalMap(n => F.delay(println(s"#$number >> $n")))
def start: Stream[F, Unit] =
for {
_ <- Stream.eval(ref.get) to sink
_ <- Stream.eval(ref.modify(_ + 1))
_ <- Stream.eval(ref.get) to sink
} yield ()
}
================================================
FILE: src/main/scala/com/github/gvolpe/fs2/Fifo.scala
================================================
package com.github.gvolpe.fs2
import cats.effect.{Effect, IO}
import fs2.StreamApp.ExitCode
import fs2.async.mutable.Queue
import fs2.{Scheduler, Stream, StreamApp, async}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
object FifoApp extends Fifo[IO]
/**
* Represents a FIFO (First IN First OUT) system built on top of two [[fs2.async.mutable.Queue]].
*
* q1 has a buffer size of 1 while q2 has a buffer size of 100 so you will notice the buffering when
* pulling elements out of the q2.
* */
class Fifo[F[_]: Effect] extends StreamApp[F] {
override def stream(args: List[String], requestShutdown: F[Unit]): fs2.Stream[F, ExitCode] =
Scheduler(corePoolSize = 4).flatMap { implicit S =>
for {
q1 <- Stream.eval(async.boundedQueue[F, Int](1))
q2 <- Stream.eval(async.boundedQueue[F, Int](100))
bp = new Buffering[F](q1, q2)
ec <- S.delay(Stream.emit(ExitCode.Success).covary[F], 5.seconds) concurrently bp.start.drain
} yield ec
}
}
class Buffering[F[_]](q1: Queue[F, Int], q2: Queue[F, Int])(implicit F: Effect[F]) {
def start: Stream[F, Unit] =
Stream(
Stream.range(0, 1000).covary[F] to q1.enqueue,
q1.dequeue to q2.enqueue,
//.map won't work here as you're trying to map a pure value with a side effect. Use `evalMap` instead.
q2.dequeue.evalMap(n => F.delay(println(s"Pulling out $n from Queue #2")))
).join(3)
}
================================================
FILE: src/main/scala/com/github/gvolpe/fs2/Once.scala
================================================
package com.github.gvolpe.fs2
import cats.effect.{Effect, IO}
import fs2.StreamApp.ExitCode
import fs2.async.Promise
import fs2.{Scheduler, Stream, StreamApp, async}
import scala.concurrent.ExecutionContext.Implicits.global
object OnceApp extends Once[IO]
/**
* Demonstrate the use of [[fs2.async.Promise]]
*
* Two processes will try to complete the promise at the same time but only one will succeed,
* completing the promise exactly once.
* The loser one will raise an error when trying to complete a promise already completed,
* that's why we call `attempt` on the evaluation.
*
* Notice that the loser process will remain running in the background and the program will
* end on completion of all of the inner streams.
*
* So it's a "race" in the sense that both processes will try to complete the promise at the
* same time but conceptually is different from "race". So for example, if you schedule one
* of the processes to run in 10 seconds from now, then the entire program will finish after
* 10 seconds and you can know for sure that the process completing the promise is going to
* be the first one.
* */
class Once[F[_]: Effect] extends StreamApp[F] {
override def stream(args: List[String], requestShutdown: F[Unit]): fs2.Stream[F, ExitCode] =
Scheduler(corePoolSize = 4).flatMap { implicit scheduler =>
for {
p <- Stream.eval(async.promise[F, Int])
e <- new ConcurrentCompletion[F](p).start
} yield e
}
}
class ConcurrentCompletion[F[_]](p: Promise[F, Int])(implicit F: Effect[F]) {
private def attemptPromiseCompletion(n: Int): Stream[F, Unit] =
Stream.eval(p.complete(n)).attempt.drain
def start: Stream[F, ExitCode] =
Stream(
attemptPromiseCompletion(1),
attemptPromiseCompletion(2),
Stream.eval(p.get).evalMap(n => F.delay(println(s"Result: $n")))
).join(3).drain ++ Stream.emit(ExitCode.Success)
}
================================================
FILE: src/main/scala/com/github/gvolpe/fs2/PubSub.scala
================================================
package com.github.gvolpe.fs2
import cats.effect.{Effect, IO}
import fs2.StreamApp.ExitCode
import fs2.async.mutable.{Signal, Topic}
import fs2.{Scheduler, Sink, Stream, StreamApp, async}
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
object PubSubApp extends PubSub[IO]
/**
* Single Publisher / Multiple Subscribers application implemented on top of
* [[fs2.async.mutable.Topic]] and [[fs2.async.mutable.Signal]].
*
* The program ends after 15 seconds when the signal interrupts the publishing of more events
* given that the final streaming merge halts on the end of its left stream (the publisher).
*
* - Subscriber #1 should receive 15 events + the initial empty event
* - Subscriber #2 should receive 10 events
* - Subscriber #3 should receive 5 events
* */
class PubSub[F[_]: Effect] extends StreamApp[F] {
override def stream(args: List[String], requestShutdown: F[Unit]): fs2.Stream[F, ExitCode] =
Scheduler(corePoolSize = 4).flatMap { implicit S =>
for {
topic <- Stream.eval(async.topic[F, Event](Event("")))
signal <- Stream.eval(async.signalOf[F, Boolean](false))
service = new EventService[F](topic, signal)
exitCode <- Stream(
S.delay(Stream.eval(signal.set(true)), 15.seconds),
service.startPublisher concurrently service.startSubscribers
).join(2).drain ++ Stream.emit(ExitCode.Success)
} yield exitCode
}
}
class EventService[F[_]](eventsTopic: Topic[F, Event],
interrupter: Signal[F, Boolean])
(implicit F: Effect[F], S: Scheduler) {
// Publishing events every one second until signaling interruption
def startPublisher: Stream[F, Unit] =
S.awakeEvery(1.second).flatMap { _ =>
val event = Event(System.currentTimeMillis().toString)
Stream.eval(eventsTopic.publish1(event))
}.interruptWhen(interrupter)
// Creating 3 subscribers in a different period of time and join them to run concurrently
def startSubscribers: Stream[F, Unit] = {
val s1: Stream[F, Event] = eventsTopic.subscribe(10)
val s2: Stream[F, Event] = S.delay(eventsTopic.subscribe(10), 5.seconds)
val s3: Stream[F, Event] = S.delay(eventsTopic.subscribe(10), 10.seconds)
def sink(subscriberNumber: Int): Sink[F, Event] =
_.evalMap(e => F.delay(println(s"Subscriber #$subscriberNumber processing event: $e")))
Stream(s1 to sink(1), s2 to sink(2), s3 to sink(3)).join(3)
}
}
================================================
FILE: src/main/scala/com/github/gvolpe/fs2/Resources.scala
================================================
package com.github.gvolpe.fs2
import cats.effect.{Effect, IO}
import cats.syntax.functor._
import fs2.StreamApp.ExitCode
import fs2.async.mutable.Semaphore
import fs2.{Scheduler, Stream, StreamApp, async}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
object ResourcesApp extends Resources[IO]
/**
* It demonstrates one of the possible uses of [[fs2.async.mutable.Semaphore]]
*
* Three processes are trying to access a shared resource at the same time but only one at
* a time will be granted access and the next process have to wait until the resource gets
* available again (availability is one as indicated by the semaphore counter).
*
* R1, R2 & R3 will request access of the precious resource concurrently so this could be
* one possible outcome:
*
* R1 >> Availability: 1
* R2 >> Availability: 1
* R2 >> Started | Availability: 0
* R3 >> Availability: 0
* --------------------------------
* R1 >> Started | Availability: 0
* R2 >> Done | Availability: 0
* --------------------------------
* R3 >> Started | Availability: 0
* R1 >> Done | Availability: 0
* --------------------------------
* R3 >> Done | Availability: 1
*
* This means when R1 and R2 requested the availability it was one and R2 was faster in
* getting access to the resource so it started processing. R3 was the slowest and saw
* that there was no availability from the beginning.
*
* Once R2 was done R1 started processing immediately showing no availability.
*
* Once R1 was done R3 started processing immediately showing no availability.
*
* Finally, R3 was done showing an availability of one once again.
* */
class Resources[F[_]: Effect] extends StreamApp[F] {
override def stream(args: List[String], requestShutdown: F[Unit]): fs2.Stream[F, ExitCode] =
Scheduler(corePoolSize = 4).flatMap { implicit scheduler =>
for {
s <- Stream.eval(async.semaphore[F](1))
r1 = new PreciousResource[F]("R1", s)
r2 = new PreciousResource[F]("R2", s)
r3 = new PreciousResource[F]("R3", s)
ec <- Stream(r1.use, r2.use, r3.use).join(3).drain ++ Stream.emit(ExitCode.Success)
} yield ec
}
}
class PreciousResource[F[_]: Effect](name: String, s: Semaphore[F])
(implicit S: Scheduler) {
def use: Stream[F, Unit] =
for {
_ <- Stream.eval(s.available.map(a => println(s"$name >> Availability: $a")))
_ <- Stream.eval(s.decrement)
_ <- Stream.eval(s.available.map(a => println(s"$name >> Started | Availability: $a")))
_ <- S.sleep(3.seconds)
_ <- Stream.eval(s.increment)
_ <- Stream.eval(s.available.map(a => println(s"$name >> Done | Availability: $a")))
} yield ()
}
================================================
FILE: src/main/scala/com/github/gvolpe/fs2/package.scala
================================================
package com.github.gvolpe
package object fs2 {
case class Event(value: String) extends AnyVal
}
================================================
FILE: src/main/scala/com/github/gvolpe/http4s/StreamUtils.scala
================================================
package com.github.gvolpe.http4s
import cats.effect.Sync
import fs2.Stream
trait StreamUtils[F[_]] {
def evalF[A](thunk: => A)(implicit F: Sync[F]): Stream[F, A] = Stream.eval(F.delay(thunk))
def putStrLn(value: String)(implicit F: Sync[F]): Stream[F, Unit] = evalF(println(value))
def putStr(value: String)(implicit F: Sync[F]): Stream[F, Unit] = evalF(print(value))
def env(name: String)(implicit F: Sync[F]): Stream[F, Option[String]] = evalF(sys.env.get(name))
def error(msg: String): Stream[F, String] = Stream.raiseError[String](new Exception(msg)).covary[F]
}
object StreamUtils {
implicit def syncInstance[F[_]: Sync]: StreamUtils[F] = new StreamUtils[F] {}
}
================================================
FILE: src/main/scala/com/github/gvolpe/http4s/client/MultipartClient.scala
================================================
package com.github.gvolpe.http4s.client
import java.net.URL
import cats.effect.Effect
import cats.syntax.flatMap._
import cats.syntax.functor._
import com.github.gvolpe.http4s.StreamUtils
import fs2.StreamApp.ExitCode
import fs2.{Scheduler, Stream, StreamApp}
import monix.eval.Task
import monix.execution.Scheduler.Implicits.global
import org.http4s.Method._
import org.http4s.client.blaze.Http1Client
import org.http4s.client.dsl.Http4sClientDsl
import org.http4s.headers.`Content-Type`
import org.http4s.multipart.{Multipart, Part}
import org.http4s.{MediaType, Uri}
object MultipartClient extends MultipartHttpClient[Task]
class MultipartHttpClient[F[_]](implicit F: Effect[F], S: StreamUtils[F]) extends StreamApp with Http4sClientDsl[F] {
private val image: F[URL] = F.delay(getClass.getResource("/rick.jpg"))
private def multipart(url: URL) = Multipart[F](
Vector(
Part.formData("name", "gvolpe"),
Part.fileData("rick", url, `Content-Type`(MediaType.`image/png`))
)
)
private val request =
for {
body <- image.map(multipart)
req <- POST(Uri.uri("http://localhost:8080/v1/multipart"), body)
} yield req.replaceAllHeaders(body.headers)
override def stream(args: List[String], requestShutdown: F[Unit]): Stream[F, ExitCode] = {
Scheduler(corePoolSize = 2).flatMap { implicit scheduler =>
for {
client <- Http1Client.stream[F]()
req <- Stream.eval(request)
value <- Stream.eval(client.expect[String](req))
_ <- S.evalF(println(value))
} yield ()
}.drain
}
}
================================================
FILE: src/main/scala/com/github/gvolpe/http4s/client/StreamClient.scala
================================================
package com.github.gvolpe.http4s.client
import cats.effect.Effect
import com.github.gvolpe.http4s.StreamUtils
import fs2.StreamApp.ExitCode
import fs2.{Stream, StreamApp}
import io.circe.Json
import jawn.Facade
import monix.eval.Task
import monix.execution.Scheduler.Implicits.global
import org.http4s.client.blaze.Http1Client
import org.http4s.{Request, Uri}
object StreamClient extends HttpClient[Task]
class HttpClient[F[_]](implicit F: Effect[F], S: StreamUtils[F]) extends StreamApp {
implicit val jsonFacade: Facade[Json] = io.circe.jawn.CirceSupportParser.facade
override def stream(args: List[String], requestShutdown: F[Unit]): Stream[F, ExitCode] = {
Http1Client.stream[F]().flatMap { client =>
val request = Request[F](uri = Uri.uri("http://localhost:8080/v1/dirs?depth=3"))
for {
response <- client.streaming(request)(_.body.chunks.through(fs2.text.utf8DecodeC))
_ <- S.putStr(response)
} yield ()
}.drain
}
}
================================================
FILE: src/main/scala/com/github/gvolpe/http4s/server/Module.scala
================================================
package com.github.gvolpe.http4s.server
import cats.effect.Effect
import cats.syntax.semigroupk._ // For <+>
import com.github.gvolpe.http4s.server.endpoints._
import com.github.gvolpe.http4s.server.endpoints.auth.{BasicAuthHttpEndpoint, GitHubHttpEndpoint}
import com.github.gvolpe.http4s.server.service.{FileService, GitHubService}
import fs2.Scheduler
import org.http4s.HttpService
import org.http4s.client.Client
import org.http4s.server.HttpMiddleware
import org.http4s.server.middleware.{AutoSlash, ChunkAggregator, GZip, Timeout}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
class Module[F[_]](client: Client[F])(implicit F: Effect[F], S: Scheduler) {
private val fileService = new FileService[F]
private val gitHubService = new GitHubService[F](client)
def middleware: HttpMiddleware[F] = {
{(service: HttpService[F]) => GZip(service)(F)} compose
{ service => AutoSlash(service)(F) }
}
val fileHttpEndpoint: HttpService[F] =
new FileHttpEndpoint[F](fileService).service
val nonStreamFileHttpEndpoint = ChunkAggregator(fileHttpEndpoint)
private val hexNameHttpEndpoint: HttpService[F] =
new HexNameHttpEndpoint[F].service
private val compressedEndpoints: HttpService[F] =
middleware(hexNameHttpEndpoint)
private val timeoutHttpEndpoint: HttpService[F] =
new TimeoutHttpEndpoint[F].service
private val timeoutEndpoints: HttpService[F] =
Timeout(1.second)(timeoutHttpEndpoint)
private val mediaHttpEndpoint: HttpService[F] =
new JsonXmlHttpEndpoint[F].service
private val multipartHttpEndpoint: HttpService[F] =
new MultipartHttpEndpoint[F](fileService).service
private val gitHubHttpEndpoint: HttpService[F] =
new GitHubHttpEndpoint[F](gitHubService).service
val basicAuthHttpEndpoint: HttpService[F] =
new BasicAuthHttpEndpoint[F].service
// NOTE: If you mix services wrapped in `AuthMiddleware[F, ?]` the entire namespace will be protected.
// You'll get 401 (Unauthorized) instead of 404 (Not found). Mount it separately as done in Server.
val httpServices: HttpService[F] = (
compressedEndpoints <+> timeoutEndpoints
<+> mediaHttpEndpoint <+> multipartHttpEndpoint
<+> gitHubHttpEndpoint
)
}
================================================
FILE: src/main/scala/com/github/gvolpe/http4s/server/Server.scala
================================================
package com.github.gvolpe.http4s.server
import cats.effect.Effect
import fs2.StreamApp.ExitCode
import fs2.{Scheduler, Stream, StreamApp}
import monix.eval.Task
import monix.execution.Scheduler.Implicits.global
import org.http4s.client.blaze.Http1Client
import org.http4s.server.blaze.BlazeBuilder
object Server extends HttpServer[Task]
class HttpServer[F[_]](implicit F: Effect[F]) extends StreamApp[F] {
override def stream(args: List[String], requestShutdown: F[Unit]): Stream[F, ExitCode] =
Scheduler(corePoolSize = 2).flatMap { implicit scheduler =>
for {
client <- Http1Client.stream[F]()
ctx <- Stream(new Module[F](client))
exitCode <- BlazeBuilder[F]
.bindHttp(8080, "0.0.0.0")
.mountService(ctx.fileHttpEndpoint, s"/${endpoints.ApiVersion}")
.mountService(ctx.nonStreamFileHttpEndpoint, s"/${endpoints.ApiVersion}/nonstream")
.mountService(ctx.httpServices)
.mountService(ctx.basicAuthHttpEndpoint, s"/${endpoints.ApiVersion}/protected")
.serve
} yield exitCode
}
}
================================================
FILE: src/main/scala/com/github/gvolpe/http4s/server/endpoints/FileHttpEndpoint.scala
================================================
package com.github.gvolpe.http4s.server.endpoints
import cats.Monad
import com.github.gvolpe.http4s.server.service.FileService
import org.http4s._
import org.http4s.dsl.Http4sDsl
class FileHttpEndpoint[F[_] : Monad](fileService: FileService[F]) extends Http4sDsl[F] {
object DepthQueryParamMatcher extends OptionalQueryParamDecoderMatcher[Int]("depth")
val service: HttpService[F] = HttpService {
case GET -> Root / "dirs" :? DepthQueryParamMatcher(depth) =>
Ok(fileService.homeDirectories(depth))
}
}
================================================
FILE: src/main/scala/com/github/gvolpe/http4s/server/endpoints/HexNameHttpEndpoint.scala
================================================
package com.github.gvolpe.http4s.server.endpoints
import cats.Monad
import org.http4s._
import org.http4s.dsl.Http4sDsl
class HexNameHttpEndpoint[F[_]: Monad] extends Http4sDsl[F] {
object NameQueryParamMatcher extends QueryParamDecoderMatcher[String]("name")
val service: HttpService[F] = HttpService {
case GET -> Root / ApiVersion / "hex" :? NameQueryParamMatcher(name) =>
Ok(name.getBytes("UTF-8").map("%02x".format(_)).mkString)
}
}
================================================
FILE: src/main/scala/com/github/gvolpe/http4s/server/endpoints/JsonXmlHttpEndpoint.scala
================================================
package com.github.gvolpe.http4s.server.endpoints
import cats.effect.Effect
import cats.syntax.flatMap._
import io.circe.generic.auto._
import org.http4s._
import org.http4s.circe._
import org.http4s.dsl.Http4sDsl
// Docs: http://http4s.org/v0.18/entity/
class JsonXmlHttpEndpoint[F[_]: Effect] extends Http4sDsl[F] {
implicit def jsonXmlDecoder: EntityDecoder[F, Person] = jsonOf[F, Person] orElse personXmlDecoder[F]
val service: HttpService[F] = HttpService {
case GET -> Root / ApiVersion / "media" =>
Ok("Send either json or xml via POST method. Eg: \n{\n \"name\": \"gvolpe\",\n \"age\": 30\n}\n or \n \n gvolpe\n 30\n")
case req @ POST -> Root / ApiVersion / "media" =>
req.as[Person].flatMap { person =>
Ok(s"Successfully decoded person: ${person.name}")
}
}
}
================================================
FILE: src/main/scala/com/github/gvolpe/http4s/server/endpoints/MultipartHttpEndpoint.scala
================================================
package com.github.gvolpe.http4s.server.endpoints
import cats.effect.Sync
import cats.implicits._
import com.github.gvolpe.http4s.server.service.FileService
import org.http4s._
import org.http4s.dsl.Http4sDsl
import org.http4s.multipart.Part
class MultipartHttpEndpoint[F[_]](fileService: FileService[F])
(implicit F: Sync[F]) extends Http4sDsl[F] {
val service: HttpService[F] = HttpService {
case GET -> Root / ApiVersion / "multipart" =>
Ok("Send a file (image, sound, etc) via POST Method")
case req @ POST -> Root / ApiVersion / "multipart" =>
req.decodeWith(multipart[F], strict = true) { response =>
def filterFileTypes(part: Part[F]): Boolean = {
part.headers.exists(_.value.contains("filename"))
}
val stream = response.parts.filter(filterFileTypes).traverse(fileService.store)
Ok(stream.map(_ => s"Multipart file parsed successfully > ${response.parts}"))
}
}
}
================================================
FILE: src/main/scala/com/github/gvolpe/http4s/server/endpoints/TimeoutHttpEndpoint.scala
================================================
package com.github.gvolpe.http4s.server.endpoints
import java.util.concurrent.TimeUnit
import cats.effect.Async
import fs2.Scheduler
import org.http4s._
import org.http4s.dsl.Http4sDsl
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.FiniteDuration
import scala.util.Random
class TimeoutHttpEndpoint[F[_]](implicit F: Async[F], S: Scheduler) extends Http4sDsl[F] {
val service: HttpService[F] = HttpService {
case GET -> Root / ApiVersion / "timeout" =>
val randomDuration = FiniteDuration(Random.nextInt(3) * 1000L, TimeUnit.MILLISECONDS)
S.effect.delay(Ok("delayed response"), randomDuration)
}
}
================================================
FILE: src/main/scala/com/github/gvolpe/http4s/server/endpoints/auth/AuthRepository.scala
================================================
package com.github.gvolpe.http4s.server.endpoints.auth
import cats.effect.Sync
import org.http4s.BasicCredentials
trait AuthRepository[F[_], A] {
def persist(entity: A): F[Unit]
def find(entity: A): F[Option[A]]
}
object AuthRepository {
implicit def authUserRepo[F[_]](implicit F: Sync[F]): AuthRepository[F, BasicCredentials] =
new AuthRepository[F, BasicCredentials] {
private val storage = scala.collection.mutable.Set[BasicCredentials](
BasicCredentials("gvolpe", "123456")
)
override def persist(entity: BasicCredentials): F[Unit] = F.delay(storage.add(entity))
override def find(entity: BasicCredentials): F[Option[BasicCredentials]] = F.delay(storage.find(_ == entity))
}
}
================================================
FILE: src/main/scala/com/github/gvolpe/http4s/server/endpoints/auth/BasicAuthHttpEndpoint.scala
================================================
package com.github.gvolpe.http4s.server.endpoints.auth
import cats.effect.Sync
import com.github.gvolpe.http4s.server.endpoints.ApiVersion
import org.http4s._
import org.http4s.dsl.Http4sDsl
import org.http4s.server.AuthMiddleware
import org.http4s.server.middleware.authentication.BasicAuth
// Use this header --> Authorization: Basic Z3ZvbHBlOjEyMzQ1Ng==
class BasicAuthHttpEndpoint[F[_]](implicit F: Sync[F], R: AuthRepository[F, BasicCredentials]) extends Http4sDsl[F] {
private val authedService: AuthedService[BasicCredentials, F] = AuthedService {
case GET -> Root as user =>
Ok(s"Access Granted: ${user.username}")
}
private val authMiddleware: AuthMiddleware[F, BasicCredentials] =
BasicAuth[F, BasicCredentials]("Protected Realm", R.find)
val service: HttpService[F] = authMiddleware(authedService)
}
================================================
FILE: src/main/scala/com/github/gvolpe/http4s/server/endpoints/auth/GitHubHttpEndpoint.scala
================================================
package com.github.gvolpe.http4s.server.endpoints.auth
import cats.effect.Sync
import cats.syntax.flatMap._
import cats.syntax.functor._
import com.github.gvolpe.http4s.server.endpoints.ApiVersion
import com.github.gvolpe.http4s.server.service.GitHubService
import org.http4s._
import org.http4s.dsl.Http4sDsl
class GitHubHttpEndpoint[F[_]](gitHubService: GitHubService[F])
(implicit F: Sync[F]) extends Http4sDsl[F] {
object CodeQuery extends QueryParamDecoderMatcher[String]("code")
object StateQuery extends QueryParamDecoderMatcher[String]("state")
val service: HttpService[F] = HttpService {
case GET -> Root / ApiVersion / "github" =>
Ok(gitHubService.authorize)
// OAuth2 Callback URI
case GET -> Root / ApiVersion / "login" / "github" :? CodeQuery(code) :? StateQuery(state) =>
Ok(gitHubService.accessToken(code, state).flatMap(gitHubService.userData))
.map(_.putHeaders(Header("Content-Type", "application/json")))
}
}
================================================
FILE: src/main/scala/com/github/gvolpe/http4s/server/endpoints/package.scala
================================================
package com.github.gvolpe.http4s.server
import cats.effect.Sync
import org.http4s.EntityDecoder
import scala.xml._
package object endpoints {
val ApiVersion = "v1"
case class Person(name: String, age: Int)
/**
* XML Example for Person:
*
*
* gvolpe
* 30
*
* */
object Person {
def fromXml(elem: Elem): Person = {
val name = (elem \\ "name").text
val age = (elem \\ "age").text
Person(name, age.toInt)
}
}
def personXmlDecoder[F[_]: Sync]: EntityDecoder[F, Person] =
org.http4s.scalaxml.xml[F].map(Person.fromXml)
}
================================================
FILE: src/main/scala/com/github/gvolpe/http4s/server/service/FileService.scala
================================================
package com.github.gvolpe.http4s.server.service
import java.io.File
import java.nio.file.Paths
import cats.effect.Effect
import com.github.gvolpe.http4s.StreamUtils
import fs2.Stream
import org.http4s.multipart.Part
class FileService[F[_]](implicit F: Effect[F], S: StreamUtils[F]) {
def homeDirectories(depth: Option[Int]): Stream[F, String] =
S.env("HOME").flatMap { maybePath =>
val ifEmpty = S.error("HOME environment variable not found!")
maybePath.fold(ifEmpty)(directories(_, depth.getOrElse(1)))
}
def directories(path: String, depth: Int): Stream[F, String] = {
def dir(f: File, d: Int): Stream[F, File] = {
val dirs = Stream.emits(f.listFiles().toSeq).filter(_.isDirectory).covary[F]
if (d <= 0) Stream.empty
else if (d == 1) dirs
else dirs ++ dirs.flatMap(x => dir(x, d - 1))
}
S.evalF(new File(path)).flatMap { file =>
dir(file, depth)
.map(_.getName)
.filter(!_.startsWith("."))
.intersperse("\n")
}
}
def store(part: Part[F]): Stream[F, Unit] =
for {
home <- S.evalF(sys.env.getOrElse("HOME", "/tmp"))
filename <- S.evalF(part.filename.getOrElse("sample"))
path <- S.evalF(Paths.get(s"$home/$filename"))
_ <- part.body to fs2.io.file.writeAll(path)
} yield ()
}
================================================
FILE: src/main/scala/com/github/gvolpe/http4s/server/service/GitHubService.scala
================================================
package com.github.gvolpe.http4s.server.service
import cats.effect.Sync
import cats.syntax.functor._
import com.github.gvolpe.http4s.server.endpoints.ApiVersion
import fs2.Stream
import io.circe.generic.auto._
import org.http4s.circe._
import org.http4s.client.Client
import org.http4s.client.dsl.Http4sClientDsl
import org.http4s.{Header, Request, Uri}
// See: https://developer.github.com/apps/building-oauth-apps/authorization-options-for-oauth-apps/#web-application-flow
class GitHubService[F[_]: Sync](client: Client[F]) extends Http4sClientDsl[F] {
// NEVER make this data public! This is just a demo!
private val ClientId = "959ea01cd3065cad274a"
private val ClientSecret = "53901db46451977e6331432faa2616ba24bc2550"
private val RedirectUri = s"http://localhost:8080/$ApiVersion/login/github"
case class AccessTokenResponse(access_token: String)
val authorize: Stream[F, Byte] = {
val uri = Uri.uri("https://github.com")
.withPath("/login/oauth/authorize")
.withQueryParam("client_id", ClientId)
.withQueryParam("redirect_uri", RedirectUri)
.withQueryParam("scopes", "public_repo")
.withQueryParam("state", "test_api")
client.streaming[Byte](Request[F](uri = uri))(_.body)
}
def accessToken(code: String, state: String): F[String] = {
val uri = Uri.uri("https://github.com")
.withPath("/login/oauth/access_token")
.withQueryParam("client_id", ClientId)
.withQueryParam("client_secret", ClientSecret)
.withQueryParam("code", code)
.withQueryParam("redirect_uri", RedirectUri)
.withQueryParam("state", state)
client.expect[AccessTokenResponse](Request[F](uri = uri))(jsonOf[F, AccessTokenResponse])
.map(_.access_token)
}
def userData(accessToken: String): F[String] = {
val request = Request[F](uri = Uri.uri("https://api.github.com/user"))
.putHeaders(Header("Authorization", s"token $accessToken"))
client.expect[String](request)
}
}
================================================
FILE: src/test/scala/com/github/gvolpe/http4s/IOAssertion.scala
================================================
package com.github.gvolpe.http4s
import cats.effect.IO
object IOAssertion {
def apply[A](ioa: IO[A]): A = ioa.unsafeRunSync()
}
================================================
FILE: src/test/scala/com/github/gvolpe/http4s/server/endpoints/HexNameHttpEndpointSpec.scala
================================================
package com.github.gvolpe.http4s.server.endpoints
import cats.effect.IO
import com.github.gvolpe.http4s.IOAssertion
import org.http4s.server.middleware.GZip
import org.http4s.{Header, HttpService, Query, Request, Uri}
import org.scalatest.FunSuite
// Docs: http://http4s.org/v0.18/gzip/
class HexNameHttpEndpointSpec extends FunSuite {
private val httpService: HttpService[IO] = GZip(new HexNameHttpEndpoint[IO].service)
private val CompressedLength = 74
private val NormalLength = 88
private val request = Request[IO](uri =
Uri(
path = s"/$ApiVersion/hex",
query = Query("name" -> Some("Scala is a really cool programming language!"))
)
)
test("Compressed Response") {
IOAssertion {
val gzipHeader = Header("Accept-Encoding", "gzip")
val gzipRequest = request.putHeaders(gzipHeader)
httpService(gzipRequest).value.flatMap { maybe =>
maybe.fold(IO[Unit](fail("Empty response"))) { response =>
response.as[String].map(r => assert(r.length == CompressedLength))
}
}
}
}
test("Uncompressed Response") {
IOAssertion {
httpService(request).value.flatMap { maybe =>
maybe.fold(IO[Unit](fail("Empty response"))) { response =>
response.as[String].map(r => assert(r.length == NormalLength))
}
}
}
}
}
================================================
FILE: src/test/scala/com/github/gvolpe/http4s/server/endpoints/JsonXmlHttpEndpointSpec.scala
================================================
package com.github.gvolpe.http4s.server.endpoints
import cats.effect.IO
import com.github.gvolpe.http4s.IOAssertion
import org.http4s.{Header, HttpService, Method, Request, Status, Uri}
import org.scalatest.FunSuite
class JsonXmlHttpEndpointSpec extends FunSuite {
private val httpService: HttpService[IO] = new JsonXmlHttpEndpoint[IO].service
private val jsonPerson =
"""
|{
| "name": "gvolpe",
| "age": 30
|}
""".stripMargin
private val xmlPerson =
"""
|
| gvolpe
| 30
|
""".stripMargin
private val request = Request[IO](method = Method.POST, uri = Uri(path = s"/$ApiVersion/media"))
test("json is decoded") {
IOAssertion {
val bodyRequest = request.withBody[String](jsonPerson)
bodyRequest.flatMap { req =>
httpService(req.putHeaders(Header("Content-Type", "application/json"))).value.map { maybe =>
maybe.fold(fail("Empty response")) { response =>
assert(response.status == Status.Ok)
}
}
}
}
}
test("xml is decoded") {
IOAssertion {
val bodyRequest = request.withBody[String](xmlPerson)
bodyRequest.flatMap { req =>
httpService(req.putHeaders(Header("Content-Type", "application/xml"))).value.map { maybe =>
maybe.fold(fail("Empty response")) { response =>
assert(response.status == Status.Ok)
}
}
}
}
}
test("decoding fails, no Content Type") {
IOAssertion {
val bodyRequest = request.withBody[String](jsonPerson)
bodyRequest.flatMap { req =>
httpService(req).value.attempt.map {
case Left(e) => assert(e.getMessage == "Malformed message body: Invalid XML")
case Right(_) => fail("Got a response when a failure was expected")
}
}
// Using `req.decode` gives you a response, using `req.as` throws an exception
// https://gitter.im/http4s/http4s?at=5a964662758c233504cc0fec
// bodyRequest.flatMap { req =>
// httpService(req).value.map { maybe =>
// maybe.fold(fail("Empty response")) { response =>
// assert(response.status == Status.BadRequest)
// }
// }
// }
}
}
}