Skip to content

Commit

Permalink
Add example subproject with doobie and http4s examples
Browse files Browse the repository at this point in the history
  • Loading branch information
peterneyens committed Dec 2, 2016
1 parent df3c3c0 commit 56748f9
Show file tree
Hide file tree
Showing 4 changed files with 282 additions and 16 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Expand Up @@ -15,6 +15,7 @@ script:
- sbt ++$TRAVIS_SCALA_VERSION 'fetchJS/test'
- sbt ++$TRAVIS_SCALA_VERSION 'docs/tut'
- sbt ++$TRAVIS_SCALA_VERSION 'readme/tut'
- sbt 'examples/test'
after_success:
- bash <(curl -s https://codecov.io/bash) -t 47609994-e0cd-4f3b-a28d-eb558142c3bb
- if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" ]; then sbt ++$TRAVIS_SCALA_VERSION
Expand Down
49 changes: 33 additions & 16 deletions build.sbt
Expand Up @@ -71,7 +71,7 @@ lazy val fetchJS = fetch.js

lazy val root = project
.in(file("."))
.aggregate(fetchJS, fetchJVM, fetchMonixJVM, fetchMonixJS)
.aggregate(fetchJS, fetchJVM, fetchMonixJVM, fetchMonixJS, debugJVM, debugJS)
.settings(allSettings)
.settings(noPublishSettings)

Expand All @@ -83,26 +83,25 @@ lazy val micrositeSettings = Seq(
micrositeGithubOwner := "47deg",
micrositeGithubRepo := "fetch",
micrositeHighlightTheme := "tomorrow",
micrositePalette := Map(
"brand-primary" -> "#FF518C",
"brand-secondary" -> "#2F2859",
"brand-tertiary" -> "#28224C",
"gray-dark" -> "#48474C",
"gray" -> "#8D8C92",
"gray-light" -> "#E3E2E3",
"gray-lighter" -> "#F4F3F9",
"white-color" -> "#FFFFFF"),
micrositePalette := Map("brand-primary" -> "#FF518C",
"brand-secondary" -> "#2F2859",
"brand-tertiary" -> "#28224C",
"gray-dark" -> "#48474C",
"gray" -> "#8D8C92",
"gray-light" -> "#E3E2E3",
"gray-lighter" -> "#F4F3F9",
"white-color" -> "#FFFFFF"),
includeFilter in makeSite := "*.html" | "*.css" | "*.png" | "*.jpg" | "*.gif" | "*.js" | "*.swf" | "*.md"
)

lazy val docsSettings = buildSettings ++ micrositeSettings ++ Seq(
tutScalacOptions ~= (_.filterNot(Set("-Ywarn-unused-import", "-Ywarn-dead-code"))),
tutScalacOptions ++= (scalaBinaryVersion.value match {
tutScalacOptions ~= (_.filterNot(Set("-Ywarn-unused-import", "-Ywarn-dead-code"))),
tutScalacOptions ++= (scalaBinaryVersion.value match {
case "2.10" => Seq("-Xdivergence211")
case _ => Nil
case _ => Nil
}),
aggregate in doc := true
)
aggregate in doc := true
)

lazy val docs = (project in file("docs"))
.dependsOn(fetchJVM, fetchMonixJVM, debugJVM)
Expand Down Expand Up @@ -158,4 +157,22 @@ lazy val debug = (crossProject in file("debug"))
.enablePlugins(AutomateHeaderPlugin)

lazy val debugJVM = debug.jvm
lazy val debugJS = debug.js
lazy val debugJS = debug.js

lazy val examplesSettings = Seq(
scalaVersion := "2.12.0",
libraryDependencies ++= Seq(
"org.tpolecat" %% "doobie-core-cats" % "0.3.1-M2",
"org.tpolecat" %% "doobie-h2-cats" % "0.3.1-M2",
"org.http4s" %% "http4s-blaze-client" % "0.15.0a",
"org.http4s" %% "http4s-circe" % "0.15.0a",
"io.circe" %% "circe-generic" % "0.6.1"
)
)

lazy val examples = (project in file("examples"))
.settings(moduleName := "fetch-examples")
.dependsOn(fetchJVM)
.settings(commonSettings: _*)
.settings(noPublishSettings: _*)
.settings(examplesSettings: _*)
109 changes: 109 additions & 0 deletions examples/src/test/scala/DoobieExample.scala
@@ -0,0 +1,109 @@
import cats.data.NonEmptyList
import cats.instances.list._
import cats.syntax.cartesian._
import cats.syntax.traverse._
import doobie.imports.{Query => _, _}
import doobie.h2.h2transactor._
import fs2.Task
import fs2.interop.cats._
import org.scalatest.{AsyncWordSpec, Matchers}

import scala.concurrent.{ExecutionContext, Future}

import fetch._
import fetch.implicits._

class DoobieExample extends AsyncWordSpec with Matchers {
implicit override def executionContext = ExecutionContext.Implicits.global

val createTransactor: Task[Transactor[Task]] =
H2Transactor[Task]("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", "sa", "")

case class AuthorId(id: Int)
case class Author(id: Int, name: String)

val dropTable = sql"DROP TABLE IF EXISTS author".update.run

val createTable = sql"""
CREATE TABLE author (
id INTEGER PRIMARY KEY,
name VARCHAR(20) NOT NULL UNIQUE
)
""".update.run

def addAuthor(author: Author) =
sql"INSERT INTO author (id, name) VALUES(${author.id}, ${author.name})".update.run

val authors: List[Author] =
List("William Shakespeare", "Charles Dickens", "George Orwell").zipWithIndex.map {
case (name, id) => Author(id + 1, name)
}

val xa: Transactor[Task] = (for {
xa <- createTransactor
_ <- (dropTable *> createTable *> authors.traverse(addAuthor)).transact(xa)
} yield xa).unsafeRunSync.toOption.getOrElse(
throw new Exception("Could not create test database and/or transactor")
)

implicit val authorDS = new DataSource[AuthorId, Author] {
override def name = "AuthorDoobie"
override def fetchOne(id: AuthorId): Query[Option[Author]] =
Query.async { (ok, fail) =>
fetchById(id).transact(xa).unsafeRunAsync(_.fold(fail, ok))
}
override def fetchMany(ids: NonEmptyList[AuthorId]): Query[Map[AuthorId, Author]] =
Query.async { (ok, fail) =>
fetchByIds(ids).map { authors =>
authors.map(a => AuthorId(a.id) -> a).toMap
}.transact(xa).unsafeRunAsync(_.fold(fail, ok))
}

def fetchById(id: AuthorId): ConnectionIO[Option[Author]] =
sql"SELECT * FROM author WHERE id = $id".query[Author].option

def fetchByIds(ids: NonEmptyList[AuthorId]): ConnectionIO[List[Author]] = {
implicit val idsParam = Param.many(ids)
sql"SELECT * FROM author WHERE id IN (${ids: ids.type})".query[Author].list
}

implicit val authorIdMeta: Meta[AuthorId] =
Meta[Int].xmap(AuthorId(_), _.id)
}

def author(id: Int): Fetch[Author] = Fetch(AuthorId(id))

"We can fetch one author from the DB" in {
val fetch: Fetch[Author] = author(1)
val fut: Future[(FetchEnv, Author)] = Fetch.runFetch[Future](fetch)
fut.map {
case (env, res) =>
res shouldEqual Author(1, "William Shakespeare")
env.rounds.size shouldEqual 1
}
}

"We can fetch multiple authors from the DB in parallel" in {
val fetch: Fetch[List[Author]] = List(1, 2).traverse(author)
val fut: Future[(FetchEnv, List[Author])] = Fetch.runFetch[Future](fetch)
fut.map {
case (env, res) =>
res shouldEqual Author(1, "William Shakespeare") :: Author(2, "Charles Dickens") :: Nil
env.rounds.size shouldEqual 1
}
}

"We can fetch multiple authors from the DB using a for comprehension" in {
val fetch: Fetch[List[Author]] = for {
a <- author(1)
b <- author(a.id + 1)
} yield List(a, b)
val fut: Future[(FetchEnv, List[Author])] = Fetch.runFetch[Future](fetch)
fut.map {
case (env, res) =>
res shouldEqual Author(1, "William Shakespeare") :: Author(2, "Charles Dickens") :: Nil
env.rounds.size shouldEqual 2
}
}

}
139 changes: 139 additions & 0 deletions examples/src/test/scala/Http4sExample.scala
@@ -0,0 +1,139 @@
import cats.data.NonEmptyList
import cats.instances.list._
import cats.syntax.traverse._
import io.circe._
import io.circe.generic.semiauto._
import org.http4s.circe._
import org.http4s.client.blaze._
import org.scalatest.{AsyncWordSpec, Matchers}
import scalaz.concurrent.Task

import scala.concurrent.{ExecutionContext, Future}

import fetch._
import fetch.implicits._

class HttpExample extends AsyncWordSpec with Matchers {
implicit override def executionContext = ExecutionContext.Implicits.global

// in this example we are fetching users and their posts via http using http4s
// the demo api is https://jsonplaceholder.typicode.com/

// the User and Post classes

case class UserId(id: Int)
case class PostId(id: Int)

case class User(id: UserId, name: String, username: String, email: String)
case class Post(id: PostId, userId: UserId, title: String, body: String)

// some circe decoders

implicit val userIdDecoder: Decoder[UserId] = Decoder[Int].map(UserId.apply)
implicit val postIdDecoder: Decoder[PostId] = Decoder[Int].map(PostId.apply)
implicit val userDecoder: Decoder[User] = deriveDecoder
implicit val postDecoder: Decoder[Post] = deriveDecoder

// http4s client which is used by the datasources

val client = PooledHttp1Client()

// a DataSource that can fetch Users with their UserId.

implicit val userDS = new DataSource[UserId, User] {
override def name = "UserH4s"
override def fetchOne(id: UserId): Query[Option[User]] =
Query.async { (ok, fail) =>
fetchById(id).unsafePerformAsync(_.fold(fail, ok))
}
override def fetchMany(ids: NonEmptyList[UserId]): Query[Map[UserId, User]] =
Query.async { (ok, fail) =>
fetchByIds(ids)
.map(users => users.map(user => user.id -> user).toMap)
.unsafePerformAsync(_.fold(fail, ok))
}

// fetchById and fetchByIds would probably be defined in some other module

def fetchById(id: UserId): Task[Option[User]] = {
val url = s"https://jsonplaceholder.typicode.com/users?id=${id.id}"
client.expect(url)(jsonOf[List[User]]).map(_.headOption)
}

def fetchByIds(ids: NonEmptyList[UserId]): Task[List[User]] = {
val filterIds = ids.map("id=" + _.id).toList.mkString("&")
val url = s"https://jsonplaceholder.typicode.com/users?$filterIds"
client.expect(url)(jsonOf[List[User]])
}
}

// a datasource that can fetch all the Posts using a UserId

implicit val postsForUserDS = new DataSource[UserId, List[Post]] {
override def name = "PostH4s"
override def fetchOne(id: UserId): Query[Option[List[Post]]] =
Query.async { (ok, fail) =>
fetchById(id).map(Option.apply).unsafePerformAsync(_.fold(fail, ok))
}
override def fetchMany(ids: NonEmptyList[UserId]): Query[Map[UserId, List[Post]]] =
Query.async { (ok, fail) =>
fetchByIds(ids).unsafePerformAsync(_.fold(fail, ok))
}

def fetchById(id: UserId): Task[List[Post]] = {
val url = s"https://jsonplaceholder.typicode.com/posts?userId=${id.id}"
client.expect(url)(jsonOf[List[Post]])
}

def fetchByIds(ids: NonEmptyList[UserId]): Task[Map[UserId, List[Post]]] = {
val filterIds = ids.map("userId=" + _.id).toList.mkString("&")
val url = s"https://jsonplaceholder.typicode.com/posts?$filterIds"
client.expect(url)(jsonOf[List[Post]]).map(_.groupBy(_.userId).toMap)
}
}

// some helper methods to create Fetches

def user(id: UserId): Fetch[User] = Fetch(id)
def postsForUser(id: UserId): Fetch[List[Post]] = Fetch(id)

"We can fetch one user" in {
val fetch: Fetch[User] = user(UserId(1))
val fut: Future[(FetchEnv, User)] = Fetch.runFetch[Future](fetch)
fut.map {
case (env, user) =>
println(user)
env.rounds.size shouldEqual 1
}
}

"We can fetch multiple users in parallel" in {
val fetch: Fetch[List[User]] = List(1, 2, 3).traverse(i => user(UserId(i)))
val fut = Fetch.runFetch[Future](fetch)
fut.map {
case (env, users) =>
users.foreach(println)
env.rounds.size shouldEqual 1
}
}

"We can fetch multiple users with their posts" in {
val fetch: Fetch[List[(User, List[Post])]] =
for {
users <- List(UserId(1), UserId(2)).traverse(user)
usersWithPosts <- users.traverseU { user =>
postsForUser(user.id).map(posts => (user, posts))
}
} yield usersWithPosts
val fut = Fetch.runFetch[Future](fetch)
fut.map {
case (env, userPosts) =>
userPosts.map {
case (user, posts) =>
s"${user.username} has ${posts.size} posts"
}.foreach(println)
env.rounds.size shouldEqual 2
}
}

}

0 comments on commit 56748f9

Please sign in to comment.