http4s

August 17, 2017

sbt configuration

/***
libraryDependencies += "io.argonaut" %% "argonaut-scalaz"     % "6.2"
libraryDependencies += "org.http4s"  %% "http4s-argonaut"     % "0.17.6"
libraryDependencies += "org.http4s"  %% "http4s-blaze-client" % "0.17.6"
libraryDependencies += "org.http4s"  %% "http4s-blaze-server" % "0.17.6"
libraryDependencies += "org.http4s"  %% "http4s-dsl"          % "0.17.6"
*/

Common imports

import argonaut._
import argonaut.Argonaut._
import fs2.Task
import java.net.URL
import org.http4s.HttpService
import org.http4s.Method
import org.http4s.Request
import org.http4s.Uri
import org.http4s.argonaut._
import org.http4s.client._
import org.http4s.client.blaze._
import org.http4s.dsl._
import org.http4s.server.Server
import org.http4s.server.blaze._
import scalaz._
import scalaz.Scalaz._

A type for the request/response body

case class Person(name: String, email: String, url: String)

val person: Person =
  Person( name = "James Earl Douglas"
        , email = "james@earldouglas.com"
        , url = "https://earldouglas.com"
        )
implicit val encodePerson: EncodeJson[Person] =
  EncodeJson {
    (x: Person) =>
      ("name" := x.name) ->:
      ("email" := x.email) ->:
      ("url" := x.url) ->:
      jEmptyObject
  }
implicit val decodePerson: DecodeJson[Person] =
  DecodeJson {
    (c: HCursor) =>
      for {
        name  <- (c --\ "name").as[String]
        email <- (c --\ "email").as[String] flatMap { email =>
                   DecodeResult {
                     if (email.matches("[^@]+@[^@]+")) Right(email)
                     else Left((s"invalid email $email", c.history))
                   }
                 }
        url   <- (c --\ "url").as[String]
      } yield Person( name = name
                    , email = email
                    , url = url
                    )
  }

Echo server

val echoService: HttpService =
  HttpService {
    case req @ POST -> Root / "echo" =>
      implicit val personDecoder = jsonOf[Person]
      implicit val personEncoder = jsonEncoderOf[Person]
      Ok(req.as[Person])
  }

val echoServiceBuilder: BlazeBuilder =
  BlazeBuilder.mountService(echoService, "/")

val echoServer: Server = echoServiceBuilder.run

Echo client

val httpClient: Client = PooledHttp1Client()

val echoPersonTask: Task[Person] =
  for {
    entity  <- jsonEncoderOf[Json].toEntity(person.asJson)
    request  = Request( method = Method.POST
                      , uri = Uri.uri("http://localhost:8080/echo")
                      , body = entity.body
                      )
    echoed  <- httpClient.expect(request)(jsonOf[Person])
  } yield echoed

val echoedPerson: Person = echoPersonTask.unsafeRun
println(s"\nechoed person:\n$echoedPerson")

Echo server cleanup

echoServer.shutdownNow()

Demo

This file is literate Scala, and can be run using Codedown:

$ curl -s https://earldouglas.com/posts/reference/argonaut/http4s.md |
  codedown scala > script.scala
$ sbt -Dsbt.main.class=sbt.ScriptMain script.scala

echoed person:
Person(James Earl Douglas,james@earldouglas.com,https://earldouglas.com)