From 732310f62278455dc5c28032ee3254c3e3fcae42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TANKOUA?= Date: Fri, 30 Nov 2018 11:59:41 +0100 Subject: [PATCH 1/3] Abstracting runner over Async type + creating the future module --- .../core/database/ComposeWithCompletion.scala | 47 ------------------- .../main/scala/core/database/LiftAsync.scala | 22 +++++++++ .../scala/core/database/QueryRunner.scala | 21 ++++----- .../scala/core/database/WithResource.scala | 7 +-- .../scala/module/future/LiftAsyncFuture.scala | 38 +++++++++++++++ .../main/scala/module/future/package.scala | 20 ++++++++ core/src/main/scala/module/sql/package.scala | 21 +++++++-- .../scala/module/sql/SqlQueryRunnerSpec.scala | 21 +++++---- .../sql/utils/SqlConnectionFactory.scala | 10 ++-- .../sample-app/app/wiring/AppLoader.scala | 1 + .../app/controller/TodoController.scala | 6 ++- .../app/controller/UserController.scala | 6 ++- examples/todo-app/app/wiring/AppLoader.scala | 4 +- .../scala/database/WithPlayTransaction.scala | 2 +- .../database/WithPlayTransactionSpec.scala | 1 + .../utils/WithPlayTransactionUseCases.scala | 39 ++++++++------- 16 files changed, 160 insertions(+), 106 deletions(-) delete mode 100644 core/src/main/scala/core/database/ComposeWithCompletion.scala create mode 100644 core/src/main/scala/core/database/LiftAsync.scala create mode 100644 core/src/main/scala/module/future/LiftAsyncFuture.scala create mode 100644 core/src/main/scala/module/future/package.scala diff --git a/core/src/main/scala/core/database/ComposeWithCompletion.scala b/core/src/main/scala/core/database/ComposeWithCompletion.scala deleted file mode 100644 index 10e74eb..0000000 --- a/core/src/main/scala/core/database/ComposeWithCompletion.scala +++ /dev/null @@ -1,47 +0,0 @@ -package com.zengularity.querymonad.core.database - -import scala.concurrent.{ExecutionContext, Future} -import scala.language.higherKinds - -/** - * Heavily inspired from work done by @cchantep in Acolyte (see acolyte.reactivemongo.ComposeWithCompletion) - */ -trait ComposeWithCompletion[F[_], Out] { - type Outer - - def apply[In](loaner: WithResource[In], f: In => F[Out]): Future[Outer] -} - -object ComposeWithCompletion extends LowPriorityCompose { - - type Aux[F[_], A, B] = ComposeWithCompletion[F, A] { type Outer = B } - - implicit def futureOut[A]: Aux[Future, A, A] = - new ComposeWithCompletion[Future, A] { - type Outer = A - - def apply[In]( - loaner: WithResource[In], - f: In => Future[A] - ): Future[Outer] = loaner(f) - - override val toString = "futureOut" - } - -} - -trait LowPriorityCompose { _: ComposeWithCompletion.type => - - implicit def pureOut[F[_], A]( - implicit ec: ExecutionContext - ): Aux[F, A, F[A]] = - new ComposeWithCompletion[F, A] { - type Outer = F[A] - - def apply[In](loaner: WithResource[In], f: In => F[A]): Future[Outer] = - loaner(r => Future(f(r))) - - override val toString = "pureOut" - } - -} diff --git a/core/src/main/scala/core/database/LiftAsync.scala b/core/src/main/scala/core/database/LiftAsync.scala new file mode 100644 index 0000000..a035a84 --- /dev/null +++ b/core/src/main/scala/core/database/LiftAsync.scala @@ -0,0 +1,22 @@ +package com.zengularity.querymonad.core.database + +// import scala.concurrent.{ExecutionContext, Future} +import scala.language.higherKinds + +/** + * Heavily inspired from work done by @cchantep in Acolyte (see acolyte.reactivemongo.ComposeWithCompletion) + */ +trait LiftAsync[F[_], M[_], A] { + type Outer + + def apply[Resource]( + loaner: WithResource[F, Resource], + f: Resource => M[A] + ): F[Outer] +} + +object LiftAsync { + + type Aux[F[_], M[_], A, B] = LiftAsync[F, M, A] { type Outer = B } + +} diff --git a/core/src/main/scala/core/database/QueryRunner.scala b/core/src/main/scala/core/database/QueryRunner.scala index 98959a6..04804ad 100644 --- a/core/src/main/scala/core/database/QueryRunner.scala +++ b/core/src/main/scala/core/database/QueryRunner.scala @@ -1,29 +1,28 @@ package com.zengularity.querymonad.core.database -import scala.concurrent.Future import scala.language.higherKinds /** * A class who can run a Query. */ -sealed trait QueryRunner[Resource] { +sealed trait QueryRunner[F[_], Resource] { def apply[M[_], T](query: QueryT[M, Resource, T])( - implicit compose: ComposeWithCompletion[M, T] - ): Future[compose.Outer] + implicit lift: LiftAsync[F, M, T] + ): F[lift.Outer] } object QueryRunner { - private class DefaultRunner[Resource](wr: WithResource[Resource]) - extends QueryRunner[Resource] { + private class DefaultRunner[F[_], Resource](wr: WithResource[F, Resource]) + extends QueryRunner[F, Resource] { def apply[M[_], T]( query: QueryT[M, Resource, T] - )(implicit compose: ComposeWithCompletion[M, T]): Future[compose.Outer] = - compose(wr, query.run) + )(implicit lift: LiftAsync[F, M, T]): F[lift.Outer] = + lift(wr, query.run) } // Default factory - def apply[Resource]( - wr: WithResource[Resource] - ): QueryRunner[Resource] = + def apply[F[_], Resource]( + wr: WithResource[F, Resource] + ): QueryRunner[F, Resource] = new DefaultRunner(wr) } diff --git a/core/src/main/scala/core/database/WithResource.scala b/core/src/main/scala/core/database/WithResource.scala index 06a25de..7eface3 100644 --- a/core/src/main/scala/core/database/WithResource.scala +++ b/core/src/main/scala/core/database/WithResource.scala @@ -1,7 +1,8 @@ package com.zengularity.querymonad.core.database -import scala.concurrent.Future +// import scala.concurrent.Future +import scala.language.higherKinds -trait WithResource[Resource] { - def apply[A](f: Resource => Future[A]): Future[A] +trait WithResource[F[_], Resource] { + def apply[A](f: Resource => F[A]): F[A] } diff --git a/core/src/main/scala/module/future/LiftAsyncFuture.scala b/core/src/main/scala/module/future/LiftAsyncFuture.scala new file mode 100644 index 0000000..22f9daa --- /dev/null +++ b/core/src/main/scala/module/future/LiftAsyncFuture.scala @@ -0,0 +1,38 @@ +package com.zengularity.querymonad.module.future + +import scala.concurrent.{ExecutionContext, Future} +import scala.language.higherKinds + +import com.zengularity.querymonad.core.database.LiftAsync + +trait LiftAsyncFuture extends LowPriority { + + implicit def futureOut[A]: LiftAsync.Aux[Future, Future, A, A] = + new LiftAsync[Future, Future, A] { + type Outer = A + + def apply[In]( + loaner: WithResourceF[In], + f: In => Future[A] + ): Future[Outer] = loaner(f) + + override val toString = "futureOut" + } + +} + +trait LowPriority { _: LiftAsyncFuture => + + implicit def pureOut[F[_], A]( + implicit ec: ExecutionContext + ): LiftAsync.Aux[Future, F, A, F[A]] = + new LiftAsync[Future, F, A] { + type Outer = F[A] + + def apply[In](loaner: WithResourceF[In], f: In => F[A]): Future[Outer] = + loaner(r => Future(f(r))) + + override val toString = "pureOut" + } + +} diff --git a/core/src/main/scala/module/future/package.scala b/core/src/main/scala/module/future/package.scala new file mode 100644 index 0000000..30f7590 --- /dev/null +++ b/core/src/main/scala/module/future/package.scala @@ -0,0 +1,20 @@ +package com.zengularity.querymonad.module + +import scala.concurrent.Future + +import com.zengularity.querymonad.core.database.{QueryRunner, WithResource} + +package object future { + + type WithResourceF[Resource] = WithResource[Future, Resource] + + type QueryRunnerF[Resource] = QueryRunner[Future, Resource] + + object QueryRunnerF { + def apply[Resource](wc: WithResourceF[Resource]): QueryRunnerF[Resource] = + QueryRunnerF[Resource](wc) + } + + object implicits extends LiftAsyncFuture + +} diff --git a/core/src/main/scala/module/sql/package.scala b/core/src/main/scala/module/sql/package.scala index 43b819b..887ca15 100644 --- a/core/src/main/scala/module/sql/package.scala +++ b/core/src/main/scala/module/sql/package.scala @@ -2,8 +2,8 @@ package com.zengularity.querymonad.module import java.sql.Connection -// import scala.concurrent.ExecutionContext import scala.language.higherKinds +import scala.concurrent.Future import cats.Applicative @@ -60,13 +60,24 @@ package object sql { type SqlQueryE[A, Err] = QueryE[Connection, A, Err] // Query runner aliases - type WithSqlConnection = WithResource[Connection] + type WithSqlConnection[F[_]] = WithResource[F, Connection] - type SqlQueryRunner = QueryRunner[Connection] + type SqlQueryRunner[F[_]] = QueryRunner[F, Connection] object SqlQueryRunner { - def apply(wc: WithSqlConnection): SqlQueryRunner = - QueryRunner[Connection](wc) + def apply[F[_]](wc: WithSqlConnection[F]): SqlQueryRunner[F] = + QueryRunner[F, Connection](wc) + } + + object future { + type WithSqlConnectionF = WithSqlConnection[Future] + + type SqlQueryRunnerF = SqlQueryRunner[Future] + + object SqlQueryRunnerF { + def apply(wc: WithSqlConnectionF): SqlQueryRunnerF = + SqlQueryRunner(wc) + } } } diff --git a/core/src/test/scala/module/sql/SqlQueryRunnerSpec.scala b/core/src/test/scala/module/sql/SqlQueryRunnerSpec.scala index faeed7d..eb000d2 100644 --- a/core/src/test/scala/module/sql/SqlQueryRunnerSpec.scala +++ b/core/src/test/scala/module/sql/SqlQueryRunnerSpec.scala @@ -12,9 +12,10 @@ import org.specs2.mutable.Specification import com.zengularity.querymonad.module.sql.{ SqlQuery, SqlQueryRunner, - SqlQueryT, - WithSqlConnection + SqlQueryT } +import com.zengularity.querymonad.module.future.implicits._ +import com.zengularity.querymonad.module.sql.future.WithSqlConnectionF import com.zengularity.querymonad.test.module.sql.models.{Material, Professor} import com.zengularity.querymonad.test.module.sql.utils.SqlConnectionFactory @@ -23,7 +24,7 @@ class SqlQueryRunnerSpec(implicit ee: ExecutionEnv) extends Specification { "SqlQueryRunner" should { // execute lift Queries "return integer value lift in Query using pure" in { - val withSqlConnection: WithSqlConnection = + val withSqlConnection: WithSqlConnectionF = SqlConnectionFactory.withSqlConnection(AcolyteQueryResult.Nil) val runner = SqlQueryRunner(withSqlConnection) val query = SqlQuery.pure(1) @@ -32,7 +33,7 @@ class SqlQueryRunnerSpec(implicit ee: ExecutionEnv) extends Specification { } "return optional value lift in Query using liftF" in { - val withSqlConnection: WithSqlConnection = + val withSqlConnection: WithSqlConnectionF = SqlConnectionFactory.withSqlConnection(AcolyteQueryResult.Nil) val runner = SqlQueryRunner(withSqlConnection) val query = SqlQueryT.liftF(Seq(1)) @@ -42,7 +43,7 @@ class SqlQueryRunnerSpec(implicit ee: ExecutionEnv) extends Specification { // execute single query "retrieve professor with id 1" in { - val withSqlConnection: WithSqlConnection = + val withSqlConnection: WithSqlConnectionF = SqlConnectionFactory.withSqlConnection(Professor.resultSet) val runner = SqlQueryRunner(withSqlConnection) val result = runner(Professor.fetchProfessor(1)).map(_.get) @@ -53,7 +54,7 @@ class SqlQueryRunnerSpec(implicit ee: ExecutionEnv) extends Specification { } "retrieve material with id 1" in { - val withSqlConnection: WithSqlConnection = + val withSqlConnection: WithSqlConnectionF = SqlConnectionFactory.withSqlConnection(Material.resultSet) val runner = SqlQueryRunner(withSqlConnection) val result = runner(Material.fetchMaterial(1)).map(_.get) @@ -64,7 +65,7 @@ class SqlQueryRunnerSpec(implicit ee: ExecutionEnv) extends Specification { } "not retrieve professor with id 2" in { - val withSqlConnection: WithSqlConnection = + val withSqlConnection: WithSqlConnectionF = SqlConnectionFactory.withSqlConnection(AcolyteQueryResult.Nil) val runner = SqlQueryRunner(withSqlConnection) val query = for { @@ -87,7 +88,7 @@ class SqlQueryRunnerSpec(implicit ee: ExecutionEnv) extends Specification { case _ => AcolyteQueryResult.Nil } - val withSqlConnection: WithSqlConnection = + val withSqlConnection: WithSqlConnectionF = SqlConnectionFactory.withSqlConnection(handler) val runner = SqlQueryRunner(withSqlConnection) val query = for { @@ -109,7 +110,7 @@ class SqlQueryRunnerSpec(implicit ee: ExecutionEnv) extends Specification { case _ => AcolyteQueryResult.Nil } - val withSqlConnection: WithSqlConnection = + val withSqlConnection: WithSqlConnectionF = SqlConnectionFactory.withSqlConnection(handler) val runner = SqlQueryRunner(withSqlConnection) val query = for { @@ -132,7 +133,7 @@ class SqlQueryRunnerSpec(implicit ee: ExecutionEnv) extends Specification { val queryResult: AcolyteQueryResult = (RowLists.rowList1(classOf[Int] -> "res").append(5)) - val withSqlConnection: WithSqlConnection = + val withSqlConnection: WithSqlConnectionF = SqlConnectionFactory.withSqlConnection(queryResult) val runner = SqlQueryRunner(withSqlConnection) val query = diff --git a/core/src/test/scala/module/sql/utils/SqlConnectionFactory.scala b/core/src/test/scala/module/sql/utils/SqlConnectionFactory.scala index 9be217d..0561d27 100644 --- a/core/src/test/scala/module/sql/utils/SqlConnectionFactory.scala +++ b/core/src/test/scala/module/sql/utils/SqlConnectionFactory.scala @@ -11,14 +11,14 @@ import acolyte.jdbc.{ ScalaCompositeHandler } -import com.zengularity.querymonad.module.sql.WithSqlConnection +import com.zengularity.querymonad.module.sql.future.WithSqlConnectionF object SqlConnectionFactory { def withSqlConnection[A <: AcolyteQueryResult]( resultsSet: A - ): WithSqlConnection = - new WithSqlConnection { + ): WithSqlConnectionF = + new WithSqlConnectionF { def apply[B](f: Connection => Future[B]): Future[B] = AcolyteDSL.withQueryResult(resultsSet) { connection => f(connection).andThen { case _ => connection.close() } @@ -26,8 +26,8 @@ object SqlConnectionFactory { } - def withSqlConnection(handler: ScalaCompositeHandler): WithSqlConnection = - new WithSqlConnection { + def withSqlConnection(handler: ScalaCompositeHandler): WithSqlConnectionF = + new WithSqlConnectionF { def apply[B](f: Connection => Future[B]): Future[B] = { val con = AcolyteDSL.connection(handler) f(con).andThen { case _ => con.close() } diff --git a/examples/sample-app/app/wiring/AppLoader.scala b/examples/sample-app/app/wiring/AppLoader.scala index 1e96545..30d3b82 100644 --- a/examples/sample-app/app/wiring/AppLoader.scala +++ b/examples/sample-app/app/wiring/AppLoader.scala @@ -15,6 +15,7 @@ import com.zengularity.querymonad.module.sql.{ SqlQueryRunner, SqlQueryT } +import com.zengularity.querymonad.module.future.implicits._ import com.zengularity.querymonad.module.playsql.database.WithPlayTransaction class AppComponents(context: Context) diff --git a/examples/todo-app/app/controller/TodoController.scala b/examples/todo-app/app/controller/TodoController.scala index 7ab6936..747ca6f 100644 --- a/examples/todo-app/app/controller/TodoController.scala +++ b/examples/todo-app/app/controller/TodoController.scala @@ -11,10 +11,12 @@ import play.api.libs.json.Json import com.zengularity.querymonad.examples.todoapp.controller.model.AddTodoPayload import com.zengularity.querymonad.examples.todoapp.model.{Todo, User} import com.zengularity.querymonad.examples.todoapp.store.{TodoStore, UserStore} -import com.zengularity.querymonad.module.sql.{SqlQueryRunner, SqlQueryT} +import com.zengularity.querymonad.module.future.implicits._ +import com.zengularity.querymonad.module.sql.SqlQueryT +import com.zengularity.querymonad.module.sql.future.SqlQueryRunnerF class TodoController( - runner: SqlQueryRunner, + runner: SqlQueryRunnerF, todoStore: TodoStore, userStore: UserStore, cc: ControllerComponents diff --git a/examples/todo-app/app/controller/UserController.scala b/examples/todo-app/app/controller/UserController.scala index c8bd750..51871a5 100644 --- a/examples/todo-app/app/controller/UserController.scala +++ b/examples/todo-app/app/controller/UserController.scala @@ -15,10 +15,12 @@ import com.zengularity.querymonad.examples.todoapp.store.{ CredentialStore, UserStore } -import com.zengularity.querymonad.module.sql.{SqlQueryRunner, SqlQueryT} +import com.zengularity.querymonad.module.future.implicits._ +import com.zengularity.querymonad.module.sql.future.SqlQueryRunnerF +import com.zengularity.querymonad.module.sql.SqlQueryT class UserController( - runner: SqlQueryRunner, + runner: SqlQueryRunnerF, store: UserStore, credentialStore: CredentialStore, cc: ControllerComponents diff --git a/examples/todo-app/app/wiring/AppLoader.scala b/examples/todo-app/app/wiring/AppLoader.scala index e0f2961..34b2c78 100644 --- a/examples/todo-app/app/wiring/AppLoader.scala +++ b/examples/todo-app/app/wiring/AppLoader.scala @@ -7,7 +7,7 @@ import play.api.db.{DBComponents, HikariCPComponents} import play.api.routing.Router import router.Routes -import com.zengularity.querymonad.module.sql.{SqlQueryRunner} +import com.zengularity.querymonad.module.sql.future.SqlQueryRunnerF import com.zengularity.querymonad.module.playsql.database.WithPlayTransaction import com.zengularity.querymonad.examples.todoapp.controller.{ TodoController, @@ -28,7 +28,7 @@ class AppComponents(context: Context) val db = dbApi.database("default") - val queryRunner = SqlQueryRunner(new WithPlayTransaction(db)) + val queryRunner = SqlQueryRunnerF(new WithPlayTransaction(db)) // Stores val userStore: UserStore = new UserStore() diff --git a/modules/play-sql/src/main/scala/database/WithPlayTransaction.scala b/modules/play-sql/src/main/scala/database/WithPlayTransaction.scala index a7b7ead..fdf51ee 100644 --- a/modules/play-sql/src/main/scala/database/WithPlayTransaction.scala +++ b/modules/play-sql/src/main/scala/database/WithPlayTransaction.scala @@ -11,7 +11,7 @@ import play.api.db.Database import com.zengularity.querymonad.module.sql.WithSqlConnection class WithPlayTransaction(db: Database)(implicit ec: ExecutionContext) - extends WithSqlConnection { + extends WithSqlConnection[Future] { val logger = Logger[WithPlayTransaction] diff --git a/modules/play-sql/src/test/scala/database/WithPlayTransactionSpec.scala b/modules/play-sql/src/test/scala/database/WithPlayTransactionSpec.scala index 6cb4903..5973ff1 100644 --- a/modules/play-sql/src/test/scala/database/WithPlayTransactionSpec.scala +++ b/modules/play-sql/src/test/scala/database/WithPlayTransactionSpec.scala @@ -6,6 +6,7 @@ import anorm._ import org.specs2.concurrent.ExecutionEnv import org.specs2.mutable.Specification +import com.zengularity.querymonad.module.future.implicits._ import com.zengularity.querymonad.module.sql.SqlQuery import com.zengularity.querymonad.test.module.playsql.utils.WithPlayTransactionUseCases._ diff --git a/modules/play-sql/src/test/scala/utils/WithPlayTransactionUseCases.scala b/modules/play-sql/src/test/scala/utils/WithPlayTransactionUseCases.scala index 30c45b6..8d99ffc 100644 --- a/modules/play-sql/src/test/scala/utils/WithPlayTransactionUseCases.scala +++ b/modules/play-sql/src/test/scala/utils/WithPlayTransactionUseCases.scala @@ -12,29 +12,32 @@ import acolyte.jdbc.RowLists.{rowList1, rowList2} import org.specs2.execute.Result import play.api.db.Database -import com.zengularity.querymonad.module.sql.{SqlQueryRunner, WithSqlConnection} +import com.zengularity.querymonad.module.sql.future.{ + SqlQueryRunnerF, + WithSqlConnectionF +} import com.zengularity.querymonad.module.playsql.database.WithPlayTransaction object WithPlayTransactionUseCases { private def testRunner[A](in: A)(f: A => Result) = f(in) - val test = testRunner[SqlQueryRunner] _ + val test = testRunner[SqlQueryRunnerF] _ - def test1 = testRunner[(SqlQueryRunner, AtomicReference[Int])] _ + def test1 = testRunner[(SqlQueryRunnerF, AtomicReference[Int])] _ - def useCase1(implicit ec: ExecutionContext): SqlQueryRunner = { + def useCase1(implicit ec: ExecutionContext): SqlQueryRunnerF = { val queryResult = rowList1( classOf[String] -> "author_name" ) :+ ("Martin Odersky") val database: Database = new AcolyteDatabase(AcolyteDSL.handleQuery(_ ⇒ queryResult)) - val withSqlConnection: WithSqlConnection = + val withSqlConnection: WithSqlConnectionF = new WithPlayTransaction(database) - SqlQueryRunner(withSqlConnection) + SqlQueryRunnerF(withSqlConnection) } - def useCase2(implicit ec: ExecutionContext): SqlQueryRunner = { + def useCase2(implicit ec: ExecutionContext): SqlQueryRunnerF = { val queryResult = rowList2( classOf[String] -> "author_name", @@ -42,14 +45,14 @@ object WithPlayTransactionUseCases { ) :+ ("Martin Odersky", "Programming in Scala") val database: Database = new AcolyteDatabase(AcolyteDSL.handleQuery(_ ⇒ queryResult)) - val withSqlConnection: WithSqlConnection = + val withSqlConnection: WithSqlConnectionF = new WithPlayTransaction(database) - SqlQueryRunner(withSqlConnection) + SqlQueryRunnerF(withSqlConnection) } def useCase3( implicit ec: ExecutionContext - ): (SqlQueryRunner, AtomicReference[Int]) = { + ): (SqlQueryRunnerF, AtomicReference[Int]) = { val step: AtomicReference[Int] = new AtomicReference(0) val handler = AcolyteDSL.handleStatement .withUpdateHandler { @@ -70,14 +73,14 @@ object WithPlayTransactionUseCases { val _ = step.compareAndSet(2, 3) }) val database: Database = new AcolyteDatabase(handler, resHandler) - val withSqlConnection: WithSqlConnection = + val withSqlConnection: WithSqlConnectionF = new WithPlayTransaction(database) - (SqlQueryRunner(withSqlConnection), step) + (SqlQueryRunnerF(withSqlConnection), step) } def useCase4( implicit ec: ExecutionContext - ): (SqlQueryRunner, AtomicReference[Int]) = { + ): (SqlQueryRunnerF, AtomicReference[Int]) = { val step: AtomicReference[Int] = new AtomicReference(0) val handler = AcolyteDSL.handleStatement .withUpdateHandler { @@ -93,14 +96,14 @@ object WithPlayTransactionUseCases { val _ = step.compareAndSet(1, 2) }) val database: Database = new AcolyteDatabase(handler, resHandler) - val withSqlConnection: WithSqlConnection = + val withSqlConnection: WithSqlConnectionF = new WithPlayTransaction(database) - (SqlQueryRunner(withSqlConnection), step) + (SqlQueryRunnerF(withSqlConnection), step) } def useCase5( implicit ec: ExecutionContext - ): (SqlQueryRunner, AtomicReference[Int]) = { + ): (SqlQueryRunnerF, AtomicReference[Int]) = { val step: AtomicReference[Int] = new AtomicReference(0) val handler = AcolyteDSL.handleStatement .withUpdateHandler { @@ -120,9 +123,9 @@ object WithPlayTransactionUseCases { val _ = step.compareAndSet(2, 3) }) val database: Database = new AcolyteDatabase(handler, resHandler) - val withSqlConnection: WithSqlConnection = + val withSqlConnection: WithSqlConnectionF = new WithPlayTransaction(database) - (SqlQueryRunner(withSqlConnection), step) + (SqlQueryRunnerF(withSqlConnection), step) } } From fe0a92e08ed40f9fa8be82897c84a34d6d7f7d87 Mon Sep 17 00:00:00 2001 From: Sebastian Sierra Date: Fri, 30 Nov 2018 14:25:23 +0100 Subject: [PATCH 2/3] Adds support for cats effects IO --- build.sbt | 10 ++ .../src/main/scala/catsio/LiftAsyncIO.scala | 37 +++++ .../src/main/scala/catsio/package.scala | 17 ++ .../src/main/scala/catsio/sql/package.scala | 17 ++ .../scala/module/sql/SqlQueryRunnerSpec.scala | 150 ++++++++++++++++++ .../sql/utils/SqlConnectionFactory.scala | 33 ++++ project/Dependencies.scala | 2 + 7 files changed, 266 insertions(+) create mode 100644 modules/cats-effect-io/src/main/scala/catsio/LiftAsyncIO.scala create mode 100644 modules/cats-effect-io/src/main/scala/catsio/package.scala create mode 100644 modules/cats-effect-io/src/main/scala/catsio/sql/package.scala create mode 100644 modules/cats-effect-io/src/test/scala/module/sql/SqlQueryRunnerSpec.scala create mode 100644 modules/cats-effect-io/src/test/scala/module/sql/utils/SqlConnectionFactory.scala diff --git a/build.sbt b/build.sbt index 398fb2a..9147d6e 100644 --- a/build.sbt +++ b/build.sbt @@ -88,6 +88,16 @@ lazy val core = (project in file("core")) ) ) +lazy val catsEffectsIOModule = (project in file("modules/cats-effect-io")) + .settings(commonSettings) + .settings( + name := "query-cats-effect-io", + libraryDependencies ++= Seq( + Dependencies.catsEffect, + ) + ) + .dependsOn(core % "test->test;compile->compile") + lazy val playSqlModule = (project in file("modules/play-sql")) .settings(commonSettings) .settings( diff --git a/modules/cats-effect-io/src/main/scala/catsio/LiftAsyncIO.scala b/modules/cats-effect-io/src/main/scala/catsio/LiftAsyncIO.scala new file mode 100644 index 0000000..c64f2cc --- /dev/null +++ b/modules/cats-effect-io/src/main/scala/catsio/LiftAsyncIO.scala @@ -0,0 +1,37 @@ +package com.zengularity.querymonad.module.catsio + +import scala.language.higherKinds + +import cats.effect.IO + +import com.zengularity.querymonad.core.database.LiftAsync + +trait LiftAsyncIO extends LowPriority { + + implicit def ioOut[A]: LiftAsync.Aux[IO, IO, A, A] = + new LiftAsync[IO, IO, A] { + type Outer = A + + def apply[In]( + loaner: WithResourceIO[In], + f: In => IO[A] + ): IO[Outer] = loaner(f) + + override val toString = "ioOut" + } + +} + +trait LowPriority { _: LiftAsyncIO => + + implicit def pureOut[F[_], A]: LiftAsync.Aux[IO, F, A, F[A]] = + new LiftAsync[IO, F, A] { + type Outer = F[A] + + def apply[In](loaner: WithResourceIO[In], f: In => F[A]): IO[Outer] = + loaner(r => IO(f(r))) + + override val toString = "pureOut" + } + +} diff --git a/modules/cats-effect-io/src/main/scala/catsio/package.scala b/modules/cats-effect-io/src/main/scala/catsio/package.scala new file mode 100644 index 0000000..0b0f30e --- /dev/null +++ b/modules/cats-effect-io/src/main/scala/catsio/package.scala @@ -0,0 +1,17 @@ +package com.zengularity.querymonad.module + +import cats.effect.IO +import com.zengularity.querymonad.core.database.{QueryRunner, WithResource} + +package object catsio { + type WithResourceIO[Resource] = WithResource[IO, Resource] + + type QueryRunnerIO[Resource] = QueryRunner[IO, Resource] + + object QueryRunnerIO { + def apply[Resource](wc: WithResourceIO[Resource]): QueryRunnerIO[Resource] = + QueryRunnerIO[Resource](wc) + } + + object implicits extends LiftAsyncIO +} diff --git a/modules/cats-effect-io/src/main/scala/catsio/sql/package.scala b/modules/cats-effect-io/src/main/scala/catsio/sql/package.scala new file mode 100644 index 0000000..7e278b8 --- /dev/null +++ b/modules/cats-effect-io/src/main/scala/catsio/sql/package.scala @@ -0,0 +1,17 @@ +package com.zengularity.querymonad.module.catsio + +import cats.effect.IO +import com.zengularity.querymonad.module.sql.{SqlQueryRunner, WithSqlConnection} + +package object sql { + + type WithSqlConnectionIO = WithSqlConnection[IO] + + type SqlQueryRunnerIO = SqlQueryRunner[IO] + + object SqlQueryRunnerIO { + def apply(wc: WithSqlConnectionIO): SqlQueryRunnerIO = + SqlQueryRunner(wc) + } + +} diff --git a/modules/cats-effect-io/src/test/scala/module/sql/SqlQueryRunnerSpec.scala b/modules/cats-effect-io/src/test/scala/module/sql/SqlQueryRunnerSpec.scala new file mode 100644 index 0000000..1c7aabf --- /dev/null +++ b/modules/cats-effect-io/src/test/scala/module/sql/SqlQueryRunnerSpec.scala @@ -0,0 +1,150 @@ +package module.sql + +import acolyte.jdbc.{ + AcolyteDSL, + ExecutedParameter, + QueryExecution, + QueryResult => AcolyteQueryResult +} +import cats.effect.IO +import com.zengularity.querymonad.module.catsio.sql.WithSqlConnectionIO +import org.specs2.mutable.Specification +import com.zengularity.querymonad.module.sql.{ + SqlQuery, + SqlQueryRunner, + SqlQueryT +} +import com.zengularity.querymonad.test.module.catsio.sql.utils.SqlConnectionFactory +import com.zengularity.querymonad.module.catsio.implicits._ +import com.zengularity.querymonad.test.module.sql.models.{Material, Professor} + +class SqlQueryRunnerSpec extends Specification { + + "SqlQueryRunner" should { + // execute lift Queries + "return integer value lift in Query using pure" in { + val withSqlConnection: WithSqlConnectionIO = + SqlConnectionFactory.withSqlConnection(AcolyteQueryResult.Nil) + val runner = SqlQueryRunner(withSqlConnection) + val query = SqlQuery.pure(1) + + runner(query).unsafeRunSync() aka "material" must beTypedEqualTo(1) + } + + "return optional value lift in Query using liftF" in { + val withSqlConnection: WithSqlConnectionIO = + SqlConnectionFactory.withSqlConnection(AcolyteQueryResult.Nil) + val runner = SqlQueryRunner(withSqlConnection) + val query = SqlQueryT.liftF(Seq(1)) + + runner(query).unsafeRunSync() aka "material" must beTypedEqualTo(Seq(1)) + } + + // execute single query + "retrieve professor with id 1" in { + val withSqlConnection: WithSqlConnectionIO = + SqlConnectionFactory.withSqlConnection(Professor.resultSet) + val runner = SqlQueryRunner(withSqlConnection) + val result = runner(Professor.fetchProfessor(1)).map(_.get) + + result.unsafeRunSync() aka "professor" must beTypedEqualTo( + Professor(1, "John Doe", 35, 1) + ) + } + + "retrieve material with id 1" in { + val withSqlConnection: WithSqlConnectionIO = + SqlConnectionFactory.withSqlConnection(Material.resultSet) + val runner = SqlQueryRunner(withSqlConnection) + val result = runner(Material.fetchMaterial(1)).map(_.get) + + result.unsafeRunSync() aka "material" must beTypedEqualTo( + Material(1, "Computer Science", 20, "Beginner") + ) + } + + "not retrieve professor with id 2" in { + val withSqlConnection: WithSqlConnectionIO = + SqlConnectionFactory.withSqlConnection(AcolyteQueryResult.Nil) + val runner = SqlQueryRunner(withSqlConnection) + val query = for { + _ <- SqlQuery.ask + professor <- Professor.fetchProfessor(2) + } yield professor + + runner(query).unsafeRunSync() aka "material" must beNone + } + + // execute composed queries into a single transaction + "retrieve professor with id 1 and his material" in { + val handler = AcolyteDSL.handleQuery { + case QueryExecution("SELECT * FROM professors where id = ?", + ExecutedParameter(1) :: Nil) => + Professor.resultSet + case QueryExecution("SELECT * FROM materials where id = ?", + ExecutedParameter(1) :: Nil) => + Material.resultSet + case _ => + AcolyteQueryResult.Nil + } + val withSqlConnection: WithSqlConnectionIO = + SqlConnectionFactory.withSqlConnection(handler) + val runner = SqlQueryRunner(withSqlConnection) + val query = for { + professor <- Professor.fetchProfessor(1).map(_.get) + material <- Material.fetchMaterial(professor.material).map(_.get) + } yield (professor, material) + + runner(query) + .unsafeRunSync() aka "professor and material" must beTypedEqualTo( + Tuple2(Professor(1, "John Doe", 35, 1), + Material(1, "Computer Science", 20, "Beginner")) + ) + } + + "not retrieve professor with id 1 and no material" in { + import cats.instances.option._ + val handler = AcolyteDSL.handleQuery { + case QueryExecution("SELECT * FROM professors where id = {id}", _) => + Professor.resultSet + case _ => + AcolyteQueryResult.Nil + } + val withSqlConnection: WithSqlConnectionIO = + SqlConnectionFactory.withSqlConnection(handler) + val runner = SqlQueryRunner(withSqlConnection) + val query = for { + professor <- SqlQueryT.fromQuery(Professor.fetchProfessor(1)) + material <- SqlQueryT.fromQuery( + Material.fetchMaterial(professor.material) + ) + } yield (professor, material) + + runner(query).unsafeRunSync() aka "professor and material" must beNone + } + + // execute async queries + "retrieve int value fetch in an async context" in { + import anorm.{SQL, SqlParser} + import acolyte.jdbc.RowLists + import acolyte.jdbc.Implicits._ + + val queryResult: AcolyteQueryResult = + (RowLists.rowList1(classOf[Int] -> "res").append(5)) + val withSqlConnection: WithSqlConnectionIO = + SqlConnectionFactory.withSqlConnection(queryResult) + val runner = SqlQueryRunner(withSqlConnection) + val query = + SqlQueryT { implicit connection => + IO { + Thread.sleep(1000) // to simulate a slow-down + SQL("SELECT 5 as res") + .as(SqlParser.int("res").single) + } + } + + runner(query).unsafeRunSync() aka "result" must beTypedEqualTo(5) + } + } + +} diff --git a/modules/cats-effect-io/src/test/scala/module/sql/utils/SqlConnectionFactory.scala b/modules/cats-effect-io/src/test/scala/module/sql/utils/SqlConnectionFactory.scala new file mode 100644 index 0000000..a2848bb --- /dev/null +++ b/modules/cats-effect-io/src/test/scala/module/sql/utils/SqlConnectionFactory.scala @@ -0,0 +1,33 @@ +package com.zengularity.querymonad.test.module.catsio.sql.utils + +import java.sql.Connection + +import acolyte.jdbc.{ + AcolyteDSL, + ScalaCompositeHandler, + QueryResult => AcolyteQueryResult +} +import cats.effect.IO +import com.zengularity.querymonad.module.catsio.sql.WithSqlConnectionIO + +object SqlConnectionFactory { + + def withSqlConnection[A <: AcolyteQueryResult]( + resultsSet: A + ): WithSqlConnectionIO = + new WithSqlConnectionIO { + def apply[B](f: Connection => IO[B]): IO[B] = + AcolyteDSL.withQueryResult(resultsSet) { connection => + f(connection).guarantee(IO(connection.close())) + } + } + + def withSqlConnection(handler: ScalaCompositeHandler): WithSqlConnectionIO = + new WithSqlConnectionIO { + def apply[B](f: Connection => IO[B]): IO[B] = { + val connection = AcolyteDSL.connection(handler) + f(connection).guarantee(IO(connection.close())) + } + } + +} diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 3489b80..b2750c6 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -8,6 +8,8 @@ object Dependencies { lazy val anorm = "org.playframework.anorm" %% "anorm" % "2.6.2" lazy val cats = "org.typelevel" %% "cats-core" % "1.4.0" + + lazy val catsEffect = "org.typelevel" %% "cats-effect" % "1.0.0" lazy val h2 = "com.h2database" % "h2" % "1.4.197" From b6b7f4e19833939cdf92d4c31a81ca70f7ca307e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TANKOUA?= Date: Wed, 16 Jan 2019 09:03:15 +0100 Subject: [PATCH 3/3] Refactor: Use Async instead of IO for async instance of LiftAsyncIO --- build.sbt | 12 ++++++++---- .../src/main/scala/catsio/LiftAsyncIO.scala | 13 +++++++------ .../test/scala/module/sql/SqlQueryRunnerSpec.scala | 9 +++++---- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/build.sbt b/build.sbt index 9147d6e..91ddf37 100644 --- a/build.sbt +++ b/build.sbt @@ -92,9 +92,7 @@ lazy val catsEffectsIOModule = (project in file("modules/cats-effect-io")) .settings(commonSettings) .settings( name := "query-cats-effect-io", - libraryDependencies ++= Seq( - Dependencies.catsEffect, - ) + libraryDependencies ++= Seq(Dependencies.catsEffect) ) .dependsOn(core % "test->test;compile->compile") @@ -152,4 +150,10 @@ lazy val todoAppExample = (project in file("examples/todo-app")) lazy val root: Project = project .in(file(".")) - .aggregate(core, playSqlModule, sampleAppExample, todoAppExample) + .aggregate( + core, + playSqlModule, + catsEffectsIOModule, + sampleAppExample, + todoAppExample + ) diff --git a/modules/cats-effect-io/src/main/scala/catsio/LiftAsyncIO.scala b/modules/cats-effect-io/src/main/scala/catsio/LiftAsyncIO.scala index c64f2cc..1540c32 100644 --- a/modules/cats-effect-io/src/main/scala/catsio/LiftAsyncIO.scala +++ b/modules/cats-effect-io/src/main/scala/catsio/LiftAsyncIO.scala @@ -2,20 +2,21 @@ package com.zengularity.querymonad.module.catsio import scala.language.higherKinds -import cats.effect.IO +import cats.effect.{Async, IO} +import com.zengularity.querymonad.core.database.WithResource import com.zengularity.querymonad.core.database.LiftAsync trait LiftAsyncIO extends LowPriority { - implicit def ioOut[A]: LiftAsync.Aux[IO, IO, A, A] = - new LiftAsync[IO, IO, A] { + implicit def ioOut[F[_]: Async, A]: LiftAsync.Aux[F, F, A, A] = + new LiftAsync[F, F, A] { type Outer = A def apply[In]( - loaner: WithResourceIO[In], - f: In => IO[A] - ): IO[Outer] = loaner(f) + loaner: WithResource[F, In], + f: In => F[A] + ): F[Outer] = loaner(f) override val toString = "ioOut" } diff --git a/modules/cats-effect-io/src/test/scala/module/sql/SqlQueryRunnerSpec.scala b/modules/cats-effect-io/src/test/scala/module/sql/SqlQueryRunnerSpec.scala index 1c7aabf..bb7f0f4 100644 --- a/modules/cats-effect-io/src/test/scala/module/sql/SqlQueryRunnerSpec.scala +++ b/modules/cats-effect-io/src/test/scala/module/sql/SqlQueryRunnerSpec.scala @@ -7,20 +7,21 @@ import acolyte.jdbc.{ QueryResult => AcolyteQueryResult } import cats.effect.IO -import com.zengularity.querymonad.module.catsio.sql.WithSqlConnectionIO import org.specs2.mutable.Specification + +import com.zengularity.querymonad.module.catsio.sql.WithSqlConnectionIO +import com.zengularity.querymonad.module.catsio.implicits._ import com.zengularity.querymonad.module.sql.{ SqlQuery, SqlQueryRunner, SqlQueryT } import com.zengularity.querymonad.test.module.catsio.sql.utils.SqlConnectionFactory -import com.zengularity.querymonad.module.catsio.implicits._ import com.zengularity.querymonad.test.module.sql.models.{Material, Professor} -class SqlQueryRunnerSpec extends Specification { +class SqlQueryRunnerIOSpec extends Specification { - "SqlQueryRunner" should { + "SqlQueryRunnerIO" should { // execute lift Queries "return integer value lift in Query using pure" in { val withSqlConnection: WithSqlConnectionIO =