KEMBAR78
Http4s, Doobie and Circe: The Functional Web Stack | PDF
THE FUNCTIONAL WEB STACK
HTTP4S, DOOBIE & CIRCE
Gary Coady
gcoady@gilt.com
LET’S BUILD A WEBSITE
• It’s going to be CRUD (create/read/update/delete)
• How do we convert data to/from database structure?
• How do we convert data to/from JSON?
• Converting from one format to another is most of a developer’s job!
CONVERTING TO/FROM JSON
String
Instant
Int
case class MyClass(
s: String,
t: Instant,
i: Int)
“s”@JSON String
“t”@JSON Number
“i”@JSON Number
Json.obj(
“s”: Json.str,
“t”: Json.number,
“i”: Json.number)
Case Class JSON Object
CONVERTING TO/FROM JDBC
String
Instant
Int
case class MyClass(
s: String,
t: Instant,
i: Int)
[0]@text
[1]@timestamptz
[2]@bigint
JDBC ResultSet(
0: text,
1: timestamptz,
2: bigint)
Case Class ResultSet
JDBC AND JSON COMBINED
String
Instant
Int
case class MyClass(
s: String,
t: Instant,
i: Int)
[0]@text
[1]@timestamptz
[2]@bigint
JDBC ResultSet(
0: text,
1: timestamptz,
2: bigint)
Case Class ResultSet
“s”@JSON String
“t”@JSON Number
“i”@JSON Number
JSON Object
Json.obj(
“s”: Json.str,
“t”: Json.number,
“i”: Json.number)
JSON CONVERSION WITH CIRCE
• Very fast, usable JSON library
• Automatic derivation of JSON codecs for case classes
• Integration available with http4s
• http://circe.io
JSON CONVERSION WITH CIRCE
trait Encoder[A] {

def apply(a: A): Json

}
A => Json
java.time.Instant => Json
implicit val jsonEncoderInstant: Encoder[Instant] =

Encoder[Long].contramap(_.toEpochMilli)
JSON CONVERSION WITH CIRCE
trait Decoder[A] {

def apply(c: HCursor): Decoder.Result[A]

}
Json => A
implicit val jsonDecoderInstant: Decoder[Instant] =

Decoder[Long].map(Instant.ofEpochMilli)
Json => java.time.Instant
CONVERTING CASE CLASSES WITH CIRCE
Automatic
import io.circe.generic.auto._
Manual
// for case class Person(id: Int, name: String)
object Person {

implicit val decodePerson: Decoder[Person] =

Decoder.forProduct2("id", "name")(Person.apply)



implicit val encodePerson: Encoder[Person] =

Encoder.forProduct2("id", "name")(p =>

(p.id, p.name)

)

}
CONVERTING TO/FROM JSON
String
Instant
Int
case class MyClass(
s: String,
t: Instant,
i: Int)
“s”@JSON String
“t”@JSON Number
“i”@JSON Number
Json.obj(
“s”: Json.str,
“t”: Json.number,
“i”: Json.number)
Case Class JSON Object
JDBC USING DOOBIE
• Doobie is a “pure functional JDBC layer for Scala”
• Complete representation of Java JDBC API
• https://github.com/tpolecat/doobie
• tpolecat.github.io/doobie-0.2.3/00-index.html (Documentation)
JDBC MAPPING WITH DOOBIE
sealed trait Meta[A] {

/** Destination JDBC types to which values of type `A` can be written. */

def jdbcTarget: NonEmptyList[JdbcType]



/** Source JDBC types from which values of type `A` can be read. */

def jdbcSource: NonEmptyList[JdbcType]



/** Constructor for a `getXXX` operation for type `A` at a given index. */

val get: Int => RS.ResultSetIO[A]


/** Constructor for a `setXXX` operation for a given `A` at a given index. */

val set: (Int, A) => PS.PreparedStatementIO[Unit] 



/** Constructor for an `updateXXX` operation for a given `A` at a given index. */

val update: (Int, A) => RS.ResultSetIO[Unit] 



/** Constructor for a `setNull` operation for the primary JDBC type, at a given index. */

val setNull: Int => PS.PreparedStatementIO[Unit]

}
JDBC MAPPING WITH DOOBIE
implicit val metaInstant =

Meta[java.sql.Timestamp].nxmap(

_.toInstant,

(i: Instant) => new java.sql.Timestamp(i.toEpochMilli)

)
Reusing an existing Meta definition
Meta definitions exist for:
Byte
Short
Int
Boolean
String
Array[Byte]
BigDecimal
Long
Float
Double
java.math.BigDecimal
java.sql.Time
java.sql.TimeStamp
java.sql.Date
java.util.Date
CONVERTING CASE CLASSES WITH DOOBIE
Nothing extra needed!
QUERIES WITH DOOBIE
sql"select id, name from people".query[Person]
Table "public.people"
Column | Type | Modifiers
---------------+--------------------------+-----------------------------------------------------
id | integer | not null default nextval('people_id_seq'::regclass)
name | text |
Indexes:
"people_pkey" PRIMARY KEY, btree (id)
Given the table
Define a query
sql"select id, name from people where id = $id".query[Person]
Using prepared statements & variable interpolation
DEFINE THE EXPECTED RESULT SIZE
• myQuery.unique — Expect a single row from the query
• myQuery.option — Expect 0-1 rows from the query, return an Option
• myQuery.list — Return the results as a List
• myQuery.vector — Return the results as a Vector
• myQuery.process — Return the results as a stream of data
RUNNING QUERIES WITH DOOBIE
Define a Transactor for your Database
val xa = DriverManagerTransactor[Task](

"org.postgresql.Driver", "jdbc:postgresql:demo", "demo", ""

)
Run your query using the Transactor
myQuery.list.transact(xa)
Your query runs in a transaction (rollback on uncaught exception)
UPDATES WITH DOOBIE
Call .update instead of .query (returns number of rows modified)
def updatePerson(id: Int, name: String): ConnectionIO[Int] =
sql"update people set name=$name where id=$id"

.update
def updatePerson(id: Int, name: String): ConnectionIO[Person] =
sql"update people set name=$name where id=$id"

.update

.withUniqueGeneratedKeys("id", "name")
.withUniqueGeneratedKeys provides data from updated row
.withGeneratedKeys provides a stream of data, when multiple rows are updated
CONVERTING TO/FROM JDBC
String
Instant
Int
case class MyClass(
s: String,
t: Instant,
i: Int)
[0]@text
[1]@timestamptz
[2]@bigint
JDBC ResultSet(
0: text,
1: timestamptz,
2: bigint)
Case Class ResultSet
JDBC AND JSON COMBINED
String
Instant
Int
case class MyClass(
s: String,
t: Instant,
i: Int)
[0]@text
[1]@timestamptz
[2]@bigint
JDBC ResultSet(
0: text,
1: timestamptz,
2: bigint)
Case Class ResultSet
“s”@JSON String
“t”@JSON Number
“i”@JSON Number
JSON Object
Json.obj(
“s”: Json.str,
“t”: Json.number,
“i”: Json.number)
HTTP4S
• http4s is a typeful, purely functional HTTP library for client and server
applications written in Scala
• http4s.org/
• https://github.com/http4s/http4s
• https://github.com/TechEmpower/FrameworkBenchmarks/tree/master/
frameworks/Scala/http4s (from TechEmpower benchmarks)
• www.lyranthe.org/http4s/ (some programming guides)
WRITING A WEB SERVICE WITH HTTP4S
type HttpService = Service[Request, Response]
represents
Request => Task[Response]
Three requirements:
• Setup service
• Parse request
• Generate response
PATTERN MATCHING ON THE REQUEST
<METHOD> -> <PATH>
Extract Method and Path from Request
For example
case GET -> path
Root / "people" / id
Extract path components from Path
PATTERN MATCHING ON THE REQUEST
Extract typed components with extractors
import java.util.UUID



object UUIDVar {

def unapply(s: String): Option[UUID] = {

try {

UUID.fromString(s)

} catch {

case e: IllegalArgumentException =>

None

}

}

}
Root / "people" / UUIDVar(uuid)
PARSING REQUEST BODY
Register Circe as JSON decoder
implicit def circeJsonDecoder[A](implicit decoder: Decoder[A]) =
org.http4s.circe.jsonOf[A]
req.decode[Person] { person =>
...
}
Decode to a Person object
CREATING RESPONSE
Ok(Person(1, "Name"))
Output response code and any type with an EntityEncoder
Ok("Hello world")
Output response code and String as body
implicit def circeJsonEncoder[A](implicit encoder: Encoder[A]) =
org.http4s.circe.jsonEncoderOf[A]
PUTTING IT ALL TOGETHER
case class Person(id: Int, firstName: String, familyName: String, registeredAt: Instant)

case class PersonForm(firstName: String, familyName: String)
object PersonDAO {

implicit val metaInstant =

Meta[java.sql.Timestamp].nxmap(

_.toInstant,

(i: Instant) => new java.sql.Timestamp(i.toEpochMilli)

)



val listPeople: ConnectionIO[List[Person]] =

sql"select id, first_name, family_name, registered_at from people"

.query[Person]

.list



def getPerson(id: Long): ConnectionIO[Option[Person]] =

sql"select id, first_name, family_name, registered_at from people where id = $id"

.query[Person]

.option



def updatePerson(id: Int, firstName: String, familyName: String): ConnectionIO[Person] =

sql"update people set first_name=$firstName, family_name=$familyName where id=$id"

.update

.withUniqueGeneratedKeys("id", "first_name", "family_name", "registered_at")



def insertPerson(firstName: String, familyName: String, registeredAt: Instant = Instant.now()): ConnectionIO[Person] =

sql"insert into people (first_name, family_name, registered_at) values ($firstName, $familyName, $registeredAt)"

.update

.withUniqueGeneratedKeys("id", "first_name", "family_name", "registered_at")

}
PUTTING IT ALL TOGETHER
case class Person(id: Int, firstName: String, familyName: String, registeredAt: Instant)

case class PersonForm(firstName: String, familyName: String)
object DemoService {

implicit def circeJsonDecoder[A](implicit decoder: Decoder[A]) =
org.http4s.circe.jsonOf[A]


implicit def circeJsonEncoder[A](implicit encoder: Encoder[A]) =
org.http4s.circe.jsonEncoderOf[A]



def service(xa: Transactor[Task]) = HttpService {

case GET -> Root / "people" =>

Ok(PersonDAO.listPeople.transact(xa))



case GET -> Root / "people" / IntVar(id) =>

for {

person <- PersonDAO.getPerson(id).transact(xa)

result <- person.fold(NotFound())(Ok.apply)

} yield result



case req @ PUT -> Root / "people" / IntVar(id) =>

req.decode[PersonForm] { form =>

Ok(PersonDAO.updatePerson(id, form.firstName, form.familyName).transact(xa))

}



case req @ POST -> Root / "people" =>

req.decode[PersonForm] { form =>

Ok(PersonDAO.insertPerson(form.firstName, form.familyName).transact(xa))

}

}

}
PUTTING IT ALL TOGETHER
object Main {

def main(args: Array[String]): Unit = {

val xa = DriverManagerTransactor[Task](

"org.postgresql.Driver", "jdbc:postgresql:demo", "demo", ""

)



val server =

BlazeBuilder
.bindHttp(8080)

.mountService(DemoService.service(xa))

.run



server.awaitShutdown()

}

}
ADDING HTML TEMPLATING WITH TWIRL
Add to project/plugins.sbt
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.1.1")
Place templates in src/main/twirl
PACKAGING WITH SBT-NATIVE-PACKAGER
Add to project/plugins.sbt
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.1.0-RC1")
Enable AutoPlugin
enablePlugins(JavaServerAppPackaging)
STREAMING DATA WITH SCALAZ-STREAM
• Response can take a scalaz-stream Process
• Doobie can create a scalaz-stream Process from a Resultset
• Data sent incrementally using chunked Transfer-Encoding
val streamPeople: Process[ConnectionIO, Person] =

sql"select id, first_name, family_name, registered_at from people"

.query[Person]

.process
case GET -> Root / "stream" =>

Ok(PersonDAO.streamPeople.transact(xa).map(p => p.id + "n"))
QUESTIONS?
https://github.com/fiadliel/http4s-talk

Http4s, Doobie and Circe: The Functional Web Stack

  • 1.
    THE FUNCTIONAL WEBSTACK HTTP4S, DOOBIE & CIRCE Gary Coady gcoady@gilt.com
  • 2.
    LET’S BUILD AWEBSITE • It’s going to be CRUD (create/read/update/delete) • How do we convert data to/from database structure? • How do we convert data to/from JSON? • Converting from one format to another is most of a developer’s job!
  • 3.
    CONVERTING TO/FROM JSON String Instant Int caseclass MyClass( s: String, t: Instant, i: Int) “s”@JSON String “t”@JSON Number “i”@JSON Number Json.obj( “s”: Json.str, “t”: Json.number, “i”: Json.number) Case Class JSON Object
  • 4.
    CONVERTING TO/FROM JDBC String Instant Int caseclass MyClass( s: String, t: Instant, i: Int) [0]@text [1]@timestamptz [2]@bigint JDBC ResultSet( 0: text, 1: timestamptz, 2: bigint) Case Class ResultSet
  • 5.
    JDBC AND JSONCOMBINED String Instant Int case class MyClass( s: String, t: Instant, i: Int) [0]@text [1]@timestamptz [2]@bigint JDBC ResultSet( 0: text, 1: timestamptz, 2: bigint) Case Class ResultSet “s”@JSON String “t”@JSON Number “i”@JSON Number JSON Object Json.obj( “s”: Json.str, “t”: Json.number, “i”: Json.number)
  • 6.
    JSON CONVERSION WITHCIRCE • Very fast, usable JSON library • Automatic derivation of JSON codecs for case classes • Integration available with http4s • http://circe.io
  • 7.
    JSON CONVERSION WITHCIRCE trait Encoder[A] {
 def apply(a: A): Json
 } A => Json java.time.Instant => Json implicit val jsonEncoderInstant: Encoder[Instant] =
 Encoder[Long].contramap(_.toEpochMilli)
  • 8.
    JSON CONVERSION WITHCIRCE trait Decoder[A] {
 def apply(c: HCursor): Decoder.Result[A]
 } Json => A implicit val jsonDecoderInstant: Decoder[Instant] =
 Decoder[Long].map(Instant.ofEpochMilli) Json => java.time.Instant
  • 9.
    CONVERTING CASE CLASSESWITH CIRCE Automatic import io.circe.generic.auto._ Manual // for case class Person(id: Int, name: String) object Person {
 implicit val decodePerson: Decoder[Person] =
 Decoder.forProduct2("id", "name")(Person.apply)
 
 implicit val encodePerson: Encoder[Person] =
 Encoder.forProduct2("id", "name")(p =>
 (p.id, p.name)
 )
 }
  • 10.
    CONVERTING TO/FROM JSON String Instant Int caseclass MyClass( s: String, t: Instant, i: Int) “s”@JSON String “t”@JSON Number “i”@JSON Number Json.obj( “s”: Json.str, “t”: Json.number, “i”: Json.number) Case Class JSON Object
  • 11.
    JDBC USING DOOBIE •Doobie is a “pure functional JDBC layer for Scala” • Complete representation of Java JDBC API • https://github.com/tpolecat/doobie • tpolecat.github.io/doobie-0.2.3/00-index.html (Documentation)
  • 12.
    JDBC MAPPING WITHDOOBIE sealed trait Meta[A] {
 /** Destination JDBC types to which values of type `A` can be written. */
 def jdbcTarget: NonEmptyList[JdbcType]
 
 /** Source JDBC types from which values of type `A` can be read. */
 def jdbcSource: NonEmptyList[JdbcType]
 
 /** Constructor for a `getXXX` operation for type `A` at a given index. */
 val get: Int => RS.ResultSetIO[A] 
 /** Constructor for a `setXXX` operation for a given `A` at a given index. */
 val set: (Int, A) => PS.PreparedStatementIO[Unit] 
 
 /** Constructor for an `updateXXX` operation for a given `A` at a given index. */
 val update: (Int, A) => RS.ResultSetIO[Unit] 
 
 /** Constructor for a `setNull` operation for the primary JDBC type, at a given index. */
 val setNull: Int => PS.PreparedStatementIO[Unit]
 }
  • 13.
    JDBC MAPPING WITHDOOBIE implicit val metaInstant =
 Meta[java.sql.Timestamp].nxmap(
 _.toInstant,
 (i: Instant) => new java.sql.Timestamp(i.toEpochMilli)
 ) Reusing an existing Meta definition Meta definitions exist for: Byte Short Int Boolean String Array[Byte] BigDecimal Long Float Double java.math.BigDecimal java.sql.Time java.sql.TimeStamp java.sql.Date java.util.Date
  • 14.
    CONVERTING CASE CLASSESWITH DOOBIE Nothing extra needed!
  • 15.
    QUERIES WITH DOOBIE sql"selectid, name from people".query[Person] Table "public.people" Column | Type | Modifiers ---------------+--------------------------+----------------------------------------------------- id | integer | not null default nextval('people_id_seq'::regclass) name | text | Indexes: "people_pkey" PRIMARY KEY, btree (id) Given the table Define a query sql"select id, name from people where id = $id".query[Person] Using prepared statements & variable interpolation
  • 16.
    DEFINE THE EXPECTEDRESULT SIZE • myQuery.unique — Expect a single row from the query • myQuery.option — Expect 0-1 rows from the query, return an Option • myQuery.list — Return the results as a List • myQuery.vector — Return the results as a Vector • myQuery.process — Return the results as a stream of data
  • 17.
    RUNNING QUERIES WITHDOOBIE Define a Transactor for your Database val xa = DriverManagerTransactor[Task](
 "org.postgresql.Driver", "jdbc:postgresql:demo", "demo", ""
 ) Run your query using the Transactor myQuery.list.transact(xa) Your query runs in a transaction (rollback on uncaught exception)
  • 18.
    UPDATES WITH DOOBIE Call.update instead of .query (returns number of rows modified) def updatePerson(id: Int, name: String): ConnectionIO[Int] = sql"update people set name=$name where id=$id"
 .update def updatePerson(id: Int, name: String): ConnectionIO[Person] = sql"update people set name=$name where id=$id"
 .update
 .withUniqueGeneratedKeys("id", "name") .withUniqueGeneratedKeys provides data from updated row .withGeneratedKeys provides a stream of data, when multiple rows are updated
  • 19.
    CONVERTING TO/FROM JDBC String Instant Int caseclass MyClass( s: String, t: Instant, i: Int) [0]@text [1]@timestamptz [2]@bigint JDBC ResultSet( 0: text, 1: timestamptz, 2: bigint) Case Class ResultSet
  • 20.
    JDBC AND JSONCOMBINED String Instant Int case class MyClass( s: String, t: Instant, i: Int) [0]@text [1]@timestamptz [2]@bigint JDBC ResultSet( 0: text, 1: timestamptz, 2: bigint) Case Class ResultSet “s”@JSON String “t”@JSON Number “i”@JSON Number JSON Object Json.obj( “s”: Json.str, “t”: Json.number, “i”: Json.number)
  • 21.
    HTTP4S • http4s isa typeful, purely functional HTTP library for client and server applications written in Scala • http4s.org/ • https://github.com/http4s/http4s • https://github.com/TechEmpower/FrameworkBenchmarks/tree/master/ frameworks/Scala/http4s (from TechEmpower benchmarks) • www.lyranthe.org/http4s/ (some programming guides)
  • 22.
    WRITING A WEBSERVICE WITH HTTP4S type HttpService = Service[Request, Response] represents Request => Task[Response] Three requirements: • Setup service • Parse request • Generate response
  • 23.
    PATTERN MATCHING ONTHE REQUEST <METHOD> -> <PATH> Extract Method and Path from Request For example case GET -> path Root / "people" / id Extract path components from Path
  • 24.
    PATTERN MATCHING ONTHE REQUEST Extract typed components with extractors import java.util.UUID
 
 object UUIDVar {
 def unapply(s: String): Option[UUID] = {
 try {
 UUID.fromString(s)
 } catch {
 case e: IllegalArgumentException =>
 None
 }
 }
 } Root / "people" / UUIDVar(uuid)
  • 25.
    PARSING REQUEST BODY RegisterCirce as JSON decoder implicit def circeJsonDecoder[A](implicit decoder: Decoder[A]) = org.http4s.circe.jsonOf[A] req.decode[Person] { person => ... } Decode to a Person object
  • 26.
    CREATING RESPONSE Ok(Person(1, "Name")) Outputresponse code and any type with an EntityEncoder Ok("Hello world") Output response code and String as body implicit def circeJsonEncoder[A](implicit encoder: Encoder[A]) = org.http4s.circe.jsonEncoderOf[A]
  • 27.
    PUTTING IT ALLTOGETHER case class Person(id: Int, firstName: String, familyName: String, registeredAt: Instant)
 case class PersonForm(firstName: String, familyName: String) object PersonDAO {
 implicit val metaInstant =
 Meta[java.sql.Timestamp].nxmap(
 _.toInstant,
 (i: Instant) => new java.sql.Timestamp(i.toEpochMilli)
 )
 
 val listPeople: ConnectionIO[List[Person]] =
 sql"select id, first_name, family_name, registered_at from people"
 .query[Person]
 .list
 
 def getPerson(id: Long): ConnectionIO[Option[Person]] =
 sql"select id, first_name, family_name, registered_at from people where id = $id"
 .query[Person]
 .option
 
 def updatePerson(id: Int, firstName: String, familyName: String): ConnectionIO[Person] =
 sql"update people set first_name=$firstName, family_name=$familyName where id=$id"
 .update
 .withUniqueGeneratedKeys("id", "first_name", "family_name", "registered_at")
 
 def insertPerson(firstName: String, familyName: String, registeredAt: Instant = Instant.now()): ConnectionIO[Person] =
 sql"insert into people (first_name, family_name, registered_at) values ($firstName, $familyName, $registeredAt)"
 .update
 .withUniqueGeneratedKeys("id", "first_name", "family_name", "registered_at")
 }
  • 28.
    PUTTING IT ALLTOGETHER case class Person(id: Int, firstName: String, familyName: String, registeredAt: Instant)
 case class PersonForm(firstName: String, familyName: String) object DemoService {
 implicit def circeJsonDecoder[A](implicit decoder: Decoder[A]) = org.http4s.circe.jsonOf[A] 
 implicit def circeJsonEncoder[A](implicit encoder: Encoder[A]) = org.http4s.circe.jsonEncoderOf[A]
 
 def service(xa: Transactor[Task]) = HttpService {
 case GET -> Root / "people" =>
 Ok(PersonDAO.listPeople.transact(xa))
 
 case GET -> Root / "people" / IntVar(id) =>
 for {
 person <- PersonDAO.getPerson(id).transact(xa)
 result <- person.fold(NotFound())(Ok.apply)
 } yield result
 
 case req @ PUT -> Root / "people" / IntVar(id) =>
 req.decode[PersonForm] { form =>
 Ok(PersonDAO.updatePerson(id, form.firstName, form.familyName).transact(xa))
 }
 
 case req @ POST -> Root / "people" =>
 req.decode[PersonForm] { form =>
 Ok(PersonDAO.insertPerson(form.firstName, form.familyName).transact(xa))
 }
 }
 }
  • 29.
    PUTTING IT ALLTOGETHER object Main {
 def main(args: Array[String]): Unit = {
 val xa = DriverManagerTransactor[Task](
 "org.postgresql.Driver", "jdbc:postgresql:demo", "demo", ""
 )
 
 val server =
 BlazeBuilder .bindHttp(8080)
 .mountService(DemoService.service(xa))
 .run
 
 server.awaitShutdown()
 }
 }
  • 30.
    ADDING HTML TEMPLATINGWITH TWIRL Add to project/plugins.sbt addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.1.1") Place templates in src/main/twirl
  • 31.
    PACKAGING WITH SBT-NATIVE-PACKAGER Addto project/plugins.sbt addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.1.0-RC1") Enable AutoPlugin enablePlugins(JavaServerAppPackaging)
  • 32.
    STREAMING DATA WITHSCALAZ-STREAM • Response can take a scalaz-stream Process • Doobie can create a scalaz-stream Process from a Resultset • Data sent incrementally using chunked Transfer-Encoding val streamPeople: Process[ConnectionIO, Person] =
 sql"select id, first_name, family_name, registered_at from people"
 .query[Person]
 .process case GET -> Root / "stream" =>
 Ok(PersonDAO.streamPeople.transact(xa).map(p => p.id + "n"))
  • 33.