Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add example subproject with doobie and http4s examples
- Loading branch information
1 parent
df3c3c0
commit 56748f9
Showing
4 changed files
with
282 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
} | ||
|
||
} |