Skip to content

Commit 2f9961c

Browse files
committed
Merge branch 'master' of github.com:jdegoes/functional-scala
2 parents 1340f03 + db9add9 commit 2f9961c

File tree

12 files changed

+372
-17
lines changed

12 files changed

+372
-17
lines changed

build.sbt

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,34 +22,72 @@ scalaVersion := "2.12.6"
2222
addCompilerPlugin("org.spire-math" %% "kind-projector" % "0.9.6")
2323

2424
scalacOptions ++= Seq(
25-
"-deprecation"
26-
, "-unchecked"
27-
, "-encoding", "UTF-8"
28-
, "-Xlint"
29-
, "-Xverify"
30-
, "-feature"
31-
, "-Ypartial-unification"
32-
, "-Xlint:-unused"
33-
, "-language:_"
25+
"-deprecation",
26+
"-unchecked",
27+
"-encoding",
28+
"UTF-8",
29+
"-Xlint",
30+
"-Xverify",
31+
"-feature",
32+
"-Ypartial-unification",
33+
"-Xlint:-unused",
34+
"-language:_"
3435
)
3536

36-
javacOptions ++= Seq("-Xlint:unchecked", "-Xlint:deprecation", "-source", "1.7", "-target", "1.7")
37+
javacOptions ++= Seq("-Xlint:unchecked",
38+
"-Xlint:deprecation",
39+
"-source",
40+
"1.7",
41+
"-target",
42+
"1.7")
3743

38-
val ScalaZVersion = "7.2.26"
44+
val ScalaZVersion = "7.2.26"
45+
val Http4sVersion = "0.20.1"
46+
val CirceVersion = "0.12.0-M1"
47+
val DoobieVersion = "0.7.0-M5"
48+
val ZIOVersion = "1.0-RC4"
49+
val PureConfigVersion = "0.11.0"
50+
val H2Version = "1.4.199"
51+
val FlywayVersion = "6.0.0-beta2"
3952

4053
libraryDependencies ++= Seq(
4154
// -- testing --
42-
"org.scalacheck" %% "scalacheck" % "1.13.4" % "test",
43-
"org.scalatest" %% "scalatest" % "3.0.1" % "test",
55+
"org.scalacheck" %% "scalacheck" % "1.13.4" % "test",
56+
"org.scalatest" %% "scalatest" % "3.0.1" % "test",
57+
"org.specs2" %% "specs2-core" % "4.3.2" % "test",
4458
// Scalaz
45-
"org.scalaz" %% "scalaz-core" % ScalaZVersion,
59+
"org.scalaz" %% "scalaz-core" % ScalaZVersion,
60+
// ZIO
61+
"org.scalaz" %% "scalaz-zio" % ZIOVersion,
62+
"org.scalaz" %% "scalaz-zio-interop-cats" % ZIOVersion,
63+
// Http4s
64+
"org.http4s" %% "http4s-blaze-server" % Http4sVersion,
65+
"org.http4s" %% "http4s-blaze-client" % Http4sVersion,
66+
"org.http4s" %% "http4s-circe" % Http4sVersion,
67+
"org.http4s" %% "http4s-dsl" % Http4sVersion,
68+
// Circe
69+
"io.circe" %% "circe-generic" % CirceVersion,
70+
"io.circe" %% "circe-generic-extras" % CirceVersion,
71+
// Doobie
72+
"org.tpolecat" %% "doobie-core" % DoobieVersion,
73+
"org.tpolecat" %% "doobie-h2" % DoobieVersion,
74+
"org.tpolecat" %% "doobie-hikari" % DoobieVersion,
75+
"org.tpolecat" %% "doobie-h2" % DoobieVersion,
76+
// log4j
77+
"org.slf4j" % "slf4j-log4j12" % "1.7.26",
78+
//pure config
79+
"com.github.pureconfig" %% "pureconfig" % PureConfigVersion,
80+
//h2
81+
"com.h2database" % "h2" % H2Version,
82+
//flyway
83+
"org.flywaydb" % "flyway-core" % FlywayVersion,
4684
// Ammonite
47-
"com.lihaoyi" % "ammonite" % "1.1.2" % "test" cross CrossVersion.full
85+
"com.lihaoyi" % "ammonite" % "1.1.2" % "test" cross CrossVersion.full
4886
)
4987

5088
resolvers ++= Seq(
51-
"Typesafe Snapshots" at "http://repo.typesafe.com/typesafe/snapshots/",
52-
"Secured Central Repository" at "https://repo1.maven.org/maven2",
89+
"Typesafe Snapshots" at "http://repo.typesafe.com/typesafe/snapshots/",
90+
"Secured Central Repository" at "https://repo1.maven.org/maven2",
5391
Resolver.sonatypeRepo("snapshots")
5492
)
5593

src/main/resources/application.conf

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
api {
2+
endpoint = "127.0.0.1"
3+
port = 8080
4+
}
5+
6+
db-config {
7+
url = "jdbc:h2:~/test;DB_CLOSE_DELAY=-1"
8+
user = ""
9+
password = ""
10+
}

src/main/resources/log4j.properties

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
log4j.rootLogger=INFO, console
2+
3+
log4j.appender.console=org.apache.log4j.ConsoleAppender
4+
log4j.appender.console.layout=org.apache.log4j.EnhancedPatternLayout
5+
log4j.appender.console.layout.ConversionPattern=[%-t] %-15c{1.}: %m%n
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package data
2+
3+
final case class User(id: Long, name: String)
4+
5+
final case class UserNotFound(id: Int) extends Exception
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import cats.effect.ExitCode
2+
import db.Persistence
3+
import http.Api
4+
import org.http4s.implicits._
5+
import org.http4s.server.Router
6+
import org.http4s.server.blaze.BlazeServerBuilder
7+
import org.http4s.server.middleware.CORS
8+
import scalaz.zio.blocking.Blocking
9+
import scalaz.zio.clock.Clock
10+
import scalaz.zio.console.putStrLn
11+
import scalaz.zio.interop.catz._
12+
import scalaz.zio.{Task, TaskR, ZIO, _}
13+
14+
object Main extends App {
15+
16+
type AppEnvironment = Clock with Blocking with Persistence
17+
18+
type AppTask[A] = TaskR[AppEnvironment, A]
19+
20+
override def run(args: List[String]): ZIO[Environment, Nothing, Int] = {
21+
val program: ZIO[Main.Environment, Throwable, Unit] = for {
22+
conf <- configuration.loadConfig
23+
_ <- configuration.initDB(conf.dbConfig)
24+
25+
blockingEnv <- ZIO.environment[Blocking]
26+
blockingEC <- blockingEnv.blocking.blockingExecutor.map(_.asEC)
27+
28+
transactorR = configuration.mkTransactor(
29+
conf.dbConfig,
30+
Platform.executor.asEC,
31+
blockingEC
32+
)
33+
34+
httpApp = Router[AppTask](
35+
"/users" -> Api(s"${conf.api.endpoint}/users").route
36+
).orNotFound
37+
38+
server = ZIO.runtime[AppEnvironment].flatMap { implicit rts =>
39+
BlazeServerBuilder[AppTask]
40+
.bindHttp(conf.api.port, "0.0.0.0")
41+
.withHttpApp(CORS(httpApp))
42+
.serve
43+
.compile[AppTask, AppTask, ExitCode]
44+
.drain
45+
}
46+
program <- transactorR.use { transactor =>
47+
server.provideSome[Environment] { _ =>
48+
new Clock.Live with Blocking.Live
49+
with Persistence.Live {
50+
override protected def tnx: doobie.Transactor[Task] = transactor
51+
}
52+
}
53+
}
54+
} yield program
55+
56+
program.foldM(
57+
err => putStrLn(s"Execution failed with: $err") *> IO.succeed(1),
58+
_ => IO.succeed(0)
59+
)
60+
}
61+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import doobie.h2.H2Transactor
2+
import org.flywaydb.core.Flyway
3+
import pureconfig.loadConfigOrThrow
4+
import scala.concurrent.ExecutionContext
5+
import scalaz.zio.{Managed, Reservation, Task, ZIO}
6+
import pureconfig.generic.auto._
7+
8+
package object configuration {
9+
10+
case class Config(api: ApiConfig, dbConfig: DbConfig)
11+
case class ApiConfig(endpoint: String, port: Int)
12+
case class DbConfig(
13+
url: String,
14+
user: String,
15+
password: String
16+
)
17+
18+
def loadConfig: Task[Config] = Task.effect(loadConfigOrThrow[Config])
19+
def initDB(conf: DbConfig): Task[Unit] =
20+
Task.effect {
21+
Flyway
22+
.configure()
23+
.dataSource(conf.url, conf.user, conf.password)
24+
.load()
25+
.migrate()
26+
}.unit
27+
28+
def mkTransactor(
29+
conf: DbConfig,
30+
connectEC: ExecutionContext,
31+
transactEC: ExecutionContext
32+
): Managed[Throwable, H2Transactor[Task]] = {
33+
import scalaz.zio.interop.catz._
34+
35+
val xa = H2Transactor
36+
.newH2Transactor[Task](conf.url, conf.user, conf.password, connectEC, transactEC)
37+
38+
val res = xa.allocated.map {
39+
case (transactor, cleanupM) =>
40+
Reservation(ZIO.succeed(transactor), cleanupM.orDie)
41+
}.uninterruptible
42+
43+
Managed(res)
44+
}
45+
46+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package db
2+
3+
import data.{ User, UserNotFound }
4+
import doobie.{ Query0, Transactor, Update0 }
5+
import scalaz.zio._
6+
import doobie.implicits._
7+
import scalaz.zio.interop.catz._
8+
9+
/**
10+
* Persistence Service
11+
*/
12+
trait Persistence extends Serializable {
13+
val userPersistence: Persistence.Service[Any]
14+
}
15+
16+
object Persistence {
17+
trait Service[R] {
18+
def get(id: Int): TaskR[R, User]
19+
def create(user: User): TaskR[R, User]
20+
def delete(id: Int): TaskR[R, Unit]
21+
}
22+
23+
/**
24+
* Persistence Module for production using Doobie
25+
*/
26+
trait Live extends Persistence {
27+
28+
protected def tnx: Transactor[Task]
29+
30+
val userPersistence: Service[Any] = new Service[Any] {
31+
32+
def get(id: Int): Task[User] =
33+
SQL
34+
.get(id)
35+
.option
36+
.transact(tnx)
37+
.foldM(
38+
err => Task.fail(err),
39+
maybeUser => Task.require(UserNotFound(id))(Task.succeed(maybeUser))
40+
)
41+
42+
def create(user: User): Task[User] =
43+
SQL
44+
.create(user)
45+
.run
46+
.transact(tnx)
47+
.foldM(err => Task.fail(err), _ => Task.succeed(user))
48+
49+
def delete(id: Int): Task[Unit] =
50+
SQL
51+
.delete(id)
52+
.run
53+
.transact(tnx)
54+
.foldM(err => Task.fail(err), _ => Task.succeed(()))
55+
}
56+
57+
object SQL {
58+
59+
def get(id: Int): Query0[User] =
60+
sql"""SELECT * FROM USERS WHERE ID = $id """.query[User]
61+
62+
def create(user: User): Update0 =
63+
sql"""INSERT INTO USERS (id, name) VALUES (${user.id}, ${user.name})""".update
64+
65+
def delete(id: Int): Update0 =
66+
sql"""DELETE FROM USERS WHERE id = $id""".update
67+
}
68+
}
69+
70+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import data.User
2+
import scalaz.zio.{TaskR, ZIO}
3+
4+
/**
5+
* Helper that will access to the Persistence Service
6+
*/
7+
package object db extends Persistence.Service[Persistence] {
8+
9+
def get(id: Int): TaskR[Persistence, User] = ZIO.accessM(_.userPersistence.get(id))
10+
def create(user: User): TaskR[Persistence, User] = ZIO.accessM(_.userPersistence.create(user))
11+
def delete(id: Int): TaskR[Persistence, Unit] = ZIO.accessM(_.userPersistence.delete(id))
12+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package http
2+
3+
import data.User
4+
import db._
5+
import db.Persistence
6+
import io.circe.{ Decoder, Encoder }
7+
import org.http4s.{ EntityDecoder, EntityEncoder, HttpRoutes }
8+
import org.http4s.dsl.Http4sDsl
9+
import scalaz.zio._
10+
import org.http4s.circe._
11+
import scalaz.zio.interop.catz._
12+
import io.circe.generic.auto._
13+
14+
final case class Api[R <: Persistence](rootUri: String) {
15+
16+
type UserTask[A] = TaskR[R, A]
17+
18+
implicit def circeJsonDecoder[A](implicit decoder: Decoder[A]): EntityDecoder[UserTask, A] = jsonOf[UserTask, A]
19+
implicit def circeJsonEncoder[A](implicit decoder: Encoder[A]): EntityEncoder[UserTask, A] =
20+
jsonEncoderOf[UserTask, A]
21+
22+
val dsl: Http4sDsl[UserTask] = Http4sDsl[UserTask]
23+
import dsl._
24+
25+
def route: HttpRoutes[UserTask] =
26+
HttpRoutes.of[UserTask] {
27+
case GET -> Root / IntVar(id) => get(id).foldM(_ => NotFound(), Ok(_))
28+
case request @ POST -> Root =>
29+
request.decode[User] { user =>
30+
Created(create(user))
31+
}
32+
case DELETE -> Root / IntVar(id) =>
33+
delete(id).foldM(_ => NotFound(), Ok(_))
34+
}
35+
36+
}

src/test/resources/log4j.properties

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
log4j.rootLogger=INFO, console
2+
3+
log4j.appender.console=org.apache.log4j.ConsoleAppender
4+
log4j.appender.console.layout=org.apache.log4j.EnhancedPatternLayout
5+
log4j.appender.console.layout.ConversionPattern=[%-t] %-15c{1.}: %m%n
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package example.db
2+
3+
import java.util.{Timer, TimerTask}
4+
import org.specs2.Specification
5+
import org.specs2.execute.AsResult
6+
import org.specs2.specification.core.{AsExecution, Execution}
7+
import scala.concurrent.duration._
8+
import scala.concurrent.{ExecutionContext, Future}
9+
import scalaz.zio._
10+
11+
abstract class TestRuntime extends Specification with DefaultRuntime {
12+
val DefaultTimeout: Duration = 60.seconds
13+
val timer = new Timer()
14+
implicit val ec: ExecutionContext = ExecutionContext.Implicits.global
15+
16+
implicit def zioAsExecution[A: AsResult, R >: Environment, E]: AsExecution[ZIO[R, E, A]] =
17+
zio => Execution.withEnvAsync(_ => runToFutureWithTimeout(zio, DefaultTimeout))
18+
19+
protected def runToFutureWithTimeout[E, R >: Environment, A: AsResult](
20+
zio: ZIO[R, E, A],
21+
timeout: Duration
22+
): Future[A] = {
23+
val p = scala.concurrent.Promise[A]()
24+
val task = new TimerTask {
25+
override def run(): Unit =
26+
try {
27+
p.failure(new Exception("TIMEOUT: " + timeout))
28+
()
29+
} catch {
30+
case _: Throwable => ()
31+
}
32+
}
33+
timer.schedule(task, timeout.toMillis)
34+
35+
unsafeRunToFuture(zio.sandbox.mapError(FiberFailure).map(p.success))
36+
p.future
37+
}
38+
39+
40+
}

0 commit comments

Comments
 (0)