Browse Source

first commit

master
Rohan Sircar 9 months ago
commit
c702bb0773
  1. 29
      .gitignore
  2. 1
      .scalafmt.conf
  3. 175
      build.sbt
  4. 6
      modules/flyway/src/main/resources/db/migration/default/V1__create_users_table.sql
  5. 6
      modules/flyway/src/main/resources/db/migration/default/V2__add_user.sql
  6. 6
      modules/flyway/src/main/resources/db/migration/default/V3__create_cars_table.sql
  7. 6
      modules/flyway/src/main/resources/db/migration/default/V4__add_car.sql
  8. 12
      modules/flyway/src/main/resources/db/migration/default/V5__authors_books_table.sql
  9. 14
      modules/flyway/src/main/resources/db/migration/default/V6__insert_books_and_authors.sql
  10. 6
      native-image-readme.md
  11. 2
      project/build.properties
  12. 12
      project/plugins.sbt
  13. 180
      src/main/resources/META-INF/native-image/wow/doge/http4sdemo/reflect-config.json
  14. 38
      src/main/resources/application.conf
  15. 16
      src/main/resources/logback.xml
  16. 40
      src/main/scala/wow/doge/http4sdemo/HelloWorld.scala
  17. 144
      src/main/scala/wow/doge/http4sdemo/Http4sdemoRoutes.scala
  18. 50
      src/main/scala/wow/doge/http4sdemo/Http4sdemoServer.scala
  19. 47
      src/main/scala/wow/doge/http4sdemo/Jokes.scala
  20. 31
      src/main/scala/wow/doge/http4sdemo/Main.scala
  21. 98
      src/main/scala/wow/doge/http4sdemo/Migrate.scala
  22. 12
      src/main/scala/wow/doge/http4sdemo/SlickResource.scala
  23. 82
      src/main/scala/wow/doge/http4sdemo/dto/Library.scala
  24. 3
      src/main/scala/wow/doge/http4sdemo/dto/User.scala
  25. 31
      src/main/scala/wow/doge/http4sdemo/implicits/package.scala
  26. 103
      src/main/scala/wow/doge/http4sdemo/services/LibraryService.scala
  27. 25
      src/test/scala/wow/doge/http4sdemo/HelloWorldSpec.scala

29
.gitignore

@ -0,0 +1,29 @@
*.class
*.log
# sbt specific
.cache/
.history/
.lib/
dist/*
target/
lib_managed/
src_managed/
project/boot/
project/plugins/project/
metals.sbt
.metals
.bloop
.ammonite
.bsp
# Scala-IDE specific
.scala_dependencies
.worksheet
.idea/
.vscode
assets/
.attach_pid*
hs_err_pid*
*.db

1
.scalafmt.conf

@ -0,0 +1 @@
version = "2.7.4"

175
build.sbt

@ -0,0 +1,175 @@
val Http4sVersion = "0.21.16"
val CirceVersion = "0.13.0"
val MunitVersion = "0.7.20"
val LogbackVersion = "1.2.3"
val MunitCatsEffectVersion = "0.13.0"
val FlywayVersion = "7.5.3"
scalaVersion in ThisBuild := "2.13.4"
import com.github.tototoshi.sbt.slick.CodegenPlugin.autoImport.{
slickCodegenDatabasePassword,
slickCodegenDatabaseUrl,
slickCodegenJdbcDriver
}
import _root_.slick.codegen.SourceCodeGenerator
import _root_.slick.{model => m}
lazy val databaseUrl = sys.env.getOrElse(
"DB_DEFAULT_URL",
"jdbc:postgresql://localhost:5432/test_db"
)
lazy val databaseUser = sys.env.getOrElse("DB_DEFAULT_USER", "test_user")
lazy val databasePassword = sys.env.getOrElse("DB_DEFAULT_PASSWORD", "password")
lazy val flyway = (project in file("modules/flyway"))
.enablePlugins(FlywayPlugin)
.settings(
libraryDependencies += "org.flywaydb" % "flyway-core" % FlywayVersion,
flywayLocations := Seq("classpath:db/migration/default"),
flywayUrl := databaseUrl,
flywayUser := databaseUser,
flywayPassword := databasePassword,
flywayBaselineOnMigrate := true
)
lazy val root = (project in file("."))
.enablePlugins(CodegenPlugin)
.settings(
organization := "wow.doge",
name := "http4s-demo",
version := "0.0.1-SNAPSHOT",
scalacOptions ++= Seq(
"-encoding",
"UTF-8",
"-deprecation",
"-feature",
"-language:existentials",
"-language:experimental.macros",
"-language:higherKinds",
"-language:implicitConversions",
"-unchecked",
"-Xlint",
"-Ywarn-numeric-widen",
"-Ymacro-annotations",
//silence warnings for by-name implicits
"-Wconf:cat=lint-byname-implicit:s",
//give errors on non exhaustive matches
"-Wconf:msg=match may not be exhaustive:e",
"-explaintypes" // Explain type errors in more detail.
),
javacOptions ++= Seq("-source", "11", "-target", "11"),
//format: off
libraryDependencies ++= Seq(
"org.http4s" %% "http4s-blaze-server" % Http4sVersion,
"org.http4s" %% "http4s-blaze-client" % Http4sVersion,
"org.http4s" %% "http4s-circe" % Http4sVersion,
"org.http4s" %% "http4s-dsl" % Http4sVersion,
"io.circe" %% "circe-generic" % CirceVersion,
"org.scalameta" %% "munit" % MunitVersion % Test,
"org.typelevel" %% "munit-cats-effect-2" % MunitCatsEffectVersion % Test,
"ch.qos.logback" % "logback-classic" % LogbackVersion,
"org.scalameta" %% "svm-subs" % "20.2.0",
"co.fs2" %% "fs2-reactive-streams" % "2.5.0"
),
//format: on
libraryDependencies ++= Seq(
"io.monix" %% "monix" % "3.3.0",
"io.monix" %% "monix-bio" % "1.1.0",
"io.circe" %% "circe-core" % "0.13.0",
"io.circe" %% "circe-generic" % "0.13.0",
"com.softwaremill.sttp.client" %% "core" % "2.2.9",
"com.softwaremill.sttp.client" %% "monix" % "2.2.9",
"com.softwaremill.sttp.client" %% "circe" % "2.2.9",
"com.softwaremill.sttp.client" %% "httpclient-backend-monix" % "2.2.9",
"com.softwaremill.quicklens" %% "quicklens" % "1.6.1",
"com.softwaremill.common" %% "tagging" % "2.2.1",
"com.softwaremill.macwire" %% "macros" % "2.3.6" % "provided",
"com.github.valskalla" %% "odin-monix" % "0.9.1",
"com.github.valskalla" %% "odin-slf4j" % "0.9.1",
"com.github.valskalla" %% "odin-json" % "0.9.1",
"com.github.valskalla" %% "odin-extras" % "0.9.1",
"com.typesafe.scala-logging" %% "scala-logging" % "3.9.2",
"com.lihaoyi" %% "os-lib" % "0.7.1",
"com.beachape" %% "enumeratum" % "1.6.1",
"com.chuusai" %% "shapeless" % "2.3.3",
"com.lihaoyi" %% "sourcecode" % "0.2.1",
"eu.timepit" %% "refined" % "0.9.19",
"com.zaxxer" % "HikariCP" % "3.4.2",
"com.typesafe.slick" %% "slick" % "3.3.2",
"com.typesafe.slick" %% "slick-hikaricp" % "3.3.2",
"com.h2database" % "h2" % "1.4.199",
"org.postgresql" % "postgresql" % "42.2.18",
"com.github.pureconfig" %% "pureconfig" % "0.14.0",
"io.scalaland" %% "chimney" % "0.6.0",
"com.rms.miu" %% "slick-cats" % "0.10.4",
"com.kubukoz" %% "slick-effect" % "0.3.0"
),
addCompilerPlugin("org.typelevel" %% "kind-projector" % "0.10.3"),
addCompilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.1"),
ThisBuild / scalafixDependencies += "com.github.liancheng" %% "organize-imports" % "0.4.3",
inThisBuild(
List(
scalaVersion := scalaVersion.value, // 2.11.12, or 2.13.3
semanticdbEnabled := true, // enable SemanticDB
semanticdbVersion := "4.4.2" // use Scalafix compatible version
)
),
testFrameworks += new TestFramework("munit.Framework"),
assemblyMergeStrategy in assembly := {
case PathList("javax", "servlet", xs @ _*) => MergeStrategy.first
case PathList(ps @ _*) if ps.last endsWith ".html" => MergeStrategy.first
case "application.conf" => MergeStrategy.concat
case "unwanted.txt" => MergeStrategy.discard
case x if Assembly.isConfigFile(x) =>
MergeStrategy.concat
case PathList("META-INF", xs @ _*) =>
(xs map { _.toLowerCase }) match {
case ("manifest.mf" :: Nil) | ("index.list" :: Nil) |
("dependencies" :: Nil) =>
MergeStrategy.discard
case ps @ (x :: xs)
if ps.last.endsWith(".sf") || ps.last.endsWith(".dsa") =>
MergeStrategy.discard
case "plexus" :: xs =>
MergeStrategy.discard
case "services" :: xs =>
MergeStrategy.filterDistinctLines
case ("spring.schemas" :: Nil) | ("spring.handlers" :: Nil) =>
MergeStrategy.filterDistinctLines
case _ => MergeStrategy.first // Changed deduplicate to first
}
case PathList(_*) => MergeStrategy.first
}
)
.settings(
// libraryDependencies ++= Seq(
// "com.zaxxer" % "HikariCP" % "3.4.2",
// "com.typesafe.slick" %% "slick" % "3.3.2",
// "com.typesafe.slick" %% "slick-hikaricp" % "3.3.2",
// "com.h2database" % "h2" % "1.4.199"
// ),
slickCodegenDatabaseUrl := databaseUrl,
slickCodegenDatabaseUser := databaseUser,
slickCodegenDatabasePassword := databasePassword,
slickCodegenDriver := _root_.slick.jdbc.PostgresProfile,
slickCodegenJdbcDriver := "org.postgresql.Driver",
slickCodegenOutputPackage := "wow.doge.http4sdemo.slickcodegen",
slickCodegenExcludedTables := Seq("schema_version"),
slickCodegenCodeGenerator := { (model: m.Model) =>
new SourceCodeGenerator(model) {
override def Table = new Table(_) {
override def Column = new Column(_) {
override def rawType = model.tpe match {
case "java.sql.Timestamp" =>
"java.time.Instant" // kill j.s.Timestamp
case _ =>
super.rawType
}
}
}
}
},
sourceGenerators in Compile += slickCodegen.taskValue
)
.dependsOn(flyway)

6
modules/flyway/src/main/resources/db/migration/default/V1__create_users_table.sql

@ -0,0 +1,6 @@
create table "users" (
"id" VARCHAR(255) PRIMARY KEY NOT NULL,
"email" VARCHAR(1024) NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NULL
);

6
modules/flyway/src/main/resources/db/migration/default/V2__add_user.sql

@ -0,0 +1,6 @@
INSERT INTO "users" VALUES (
'd074bce8-a8ca-49ec-9225-a50ffe83dc2f',
'myuser@example.com',
(TIMESTAMP '2013-03-26T17:50:06Z'),
(TIMESTAMP '2013-03-26T17:50:06Z')
);

6
modules/flyway/src/main/resources/db/migration/default/V3__create_cars_table.sql

@ -0,0 +1,6 @@
create table "cars" (
"id" VARCHAR(255) PRIMARY KEY NOT NULL,
"model" VARCHAR(1024) NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NULL
);

6
modules/flyway/src/main/resources/db/migration/default/V4__add_car.sql

@ -0,0 +1,6 @@
INSERT INTO "cars" VALUES (
'd074bce8-a8ca-49ec-9225-a50ffe83dc2f',
'gxxer',
(TIMESTAMP '2013-03-26T17:50:06Z'),
(TIMESTAMP '2013-03-26T17:50:06Z')
);

12
modules/flyway/src/main/resources/db/migration/default/V5__authors_books_table.sql

@ -0,0 +1,12 @@
create table authors (
id SERIAL PRIMARY KEY,
name VARCHAR(15) NOT NULL
);
create table books (
id SERIAL PRIMARY KEY,
title VARCHAR(50) NOT NULL,
author_id INTEGER NOT NULL,
FOREIGN KEY(author_id) REFERENCES authors(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);

14
modules/flyway/src/main/resources/db/migration/default/V6__insert_books_and_authors.sql

@ -0,0 +1,14 @@
-- create table authors (
-- id INTEGER PRIMARY KEY NOT NULL,
-- name VARCHAR(15)
-- );
-- create table books (
-- id INTEGER PRIMARY KEY NOT NULL,
-- title VARCHAR(15) NOT NULL,
-- author_id INTEGER NOT NULL,
-- FOREIGN KEY(author_id) REFERENCES authors(id)
-- );
INSERT INTO authors (name) VALUES ('Jane Austen');
INSERT INTO books (title, author_id) VALUES ('Pride and Prejudice', 1);

6
native-image-readme.md

@ -0,0 +1,6 @@
You can build a native-image binary as mentioned in the http4s deployment [section] (https://github.com/drocsid/http4s/blob/docs/deployment/docs/src/main/tut/deployment.md) . You will need to follow the directions there to provide GraalVM / native-image plugin and provide a muslC bundle. Then populate the UseMuslC path with it's location.
```
native-image --static -H:UseMuslC="/path.to/muslC" -H:+ReportExceptionStackTraces -H:+AddAllCharsets --allow-incomplete-classpath --no-fallback --initialize-at-build-time --enable-http --enable-https --enable-all-security-services --verbose -jar "./target/scala-2.13/http4s-demo-assembly-0.0.1-SNAPSHOT.jar" http4s-demoBinaryImage
```

2
project/build.properties

@ -0,0 +1,2 @@
sbt.version=1.4.7

12
project/plugins.sbt

@ -0,0 +1,12 @@
// addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.1.14")
addSbtPlugin("io.spray" % "sbt-revolver" % "0.9.1")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.10")
// https://github.com/tototoshi/sbt-slick-codegen
libraryDependencies += "com.h2database" % "h2" % "1.4.196"
libraryDependencies += "org.postgresql" % "postgresql" % "42.2.18"
addSbtPlugin("com.github.tototoshi" % "sbt-slick-codegen" % "1.4.0")
// Database migration
// https://github.com/flyway/flyway-sbt
addSbtPlugin("io.github.davidmweber" % "flyway-sbt" % "7.4.0")
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.23")

180
src/main/resources/META-INF/native-image/wow/doge/http4sdemo/reflect-config.json

@ -0,0 +1,180 @@
[
{
"name": "org.slf4j.impl.StaticLoggerBinder",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.classic.pattern.DateConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.classic.pattern.MessageConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.classic.pattern.ThrowableProxyConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.classic.pattern.NopThrowableInformationConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.classic.pattern.ContextNameConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.core.pattern.color.BoldYellowCompositeConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.classic.pattern.LoggerConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.core.pattern.ReplacingCompositeConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.core.pattern.color.BoldBlueCompositeConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.core.pattern.color.CyanCompositeConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.core.pattern.color.RedCompositeConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.core.pattern.color.WhiteCompositeConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.classic.pattern.PropertyConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.classic.pattern.ExtendedThrowableProxyConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.classic.pattern.RootCauseFirstThrowableProxyConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.classic.pattern.MethodOfCallerConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.classic.pattern.LevelConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.core.pattern.IdentityCompositeConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.core.pattern.color.BoldWhiteCompositeConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.classic.pattern.MarkerConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.core.pattern.color.BoldCyanCompositeConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.core.pattern.color.BoldMagentaCompositeConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.classic.pattern.RelativeTimeConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.core.pattern.color.MagentaCompositeConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.classic.pattern.ClassOfCallerConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.classic.pattern.LineOfCallerConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.classic.pattern.FileOfCallerConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.core.pattern.color.BoldGreenCompositeConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.classic.pattern.LocalSequenceNumberConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.core.pattern.color.YellowCompositeConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.classic.pattern.ExtendedThrowableProxyConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.classic.pattern.color.HighlightingCompositeConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.core.pattern.color.GrayCompositeConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.classic.pattern.MDCConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.classic.pattern.ClassOfCallerConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.core.pattern.color.BoldRedCompositeConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.core.pattern.color.GreenCompositeConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.core.pattern.color.BlackCompositeConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.classic.pattern.ThreadConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.classic.pattern.LineSeparatorConverter",
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.classic.encoder.PatternLayoutEncoder",
"allPublicMethods":true,
"allDeclaredConstructors": true
},
{
"name": "ch.qos.logback.core.ConsoleAppender",
"allPublicMethods":true,
"allDeclaredConstructors": true
},
{
"name": "com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl",
"allDeclaredConstructors": true
}
]

38
src/main/resources/application.conf

@ -0,0 +1,38 @@
myapp = {
database = {
driver = org.postgresql.Driver
url = "jdbc:postgresql://localhost:5432/test_db"
user = "test_user"
password = "password"
// The number of threads determines how many things you can *run* in parallel
// the number of connections determines you many things you can *keep in memory* at the same time
// on the database server.
// numThreads = (core_count (hyperthreading included))
numThreads = 20
// queueSize = ((core_count * 2) + effective_spindle_count)
// on a MBP 13, this is 2 cores * 2 (hyperthreading not included) + 1 hard disk
queueSize = 10
// https://blog.knoldus.com/2016/01/01/best-practices-for-using-slick-on-production/
// make larger than numThreads + queueSize
maxConnections = 20
connectionTimeout = 5000
validationTimeout = 5000
# connectionPool = disabled
keepAlive = true
migrations-table = "flyway_schema_history"
migrations-locations = [
# "classpath:example/jdbc"
"classpath:db/migration/default"
]
}
}

16
src/main/resources/logback.xml

@ -0,0 +1,16 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- On Windows machines setting withJansi to true enables ANSI
color code interpretation by the Jansi library. This requires
org.fusesource.jansi:jansi:1.8 on the class path. Note that
Unix-based operating systems such as Linux and Mac OS X
support ANSI color codes by default. -->
<withJansi>true</withJansi>
<encoder>
<pattern>[%thread] %highlight(%-5level) %cyan(%logger{15}) - %msg %n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>

40
src/main/scala/wow/doge/http4sdemo/HelloWorld.scala

@ -0,0 +1,40 @@
package wow.doge.http4sdemo
import cats.Applicative
import cats.implicits._
import io.circe.Encoder
import io.circe.Json
import monix.bio.Task
import org.http4s.EntityEncoder
import org.http4s.circe._
trait HelloWorld[F[_]] {
def hello(n: HelloWorld.Name): F[HelloWorld.Greeting]
}
object HelloWorld {
implicit def apply[F[_]](implicit ev: HelloWorld[F]): HelloWorld[F] = ev
final case class Name(name: String) extends AnyVal
/** More generally you will want to decouple your edge representations from
* your internal data structures, however this shows how you can
* create encoders for your data.
*/
final case class Greeting(greeting: String) extends AnyVal
object Greeting {
implicit val greetingEncoder: Encoder[Greeting] = new Encoder[Greeting] {
final def apply(a: Greeting): Json = Json.obj(
("message", Json.fromString(a.greeting))
)
}
implicit def greetingEntityEncoder[F[_]: Applicative]
: EntityEncoder[F, Greeting] =
jsonEncoderOf[F, Greeting]
}
def impl: HelloWorld[Task] = new HelloWorld[Task] {
def hello(n: HelloWorld.Name): Task[HelloWorld.Greeting] =
Greeting("Hello, " + n.name).pure[Task]
}
}

144
src/main/scala/wow/doge/http4sdemo/Http4sdemoRoutes.scala

@ -0,0 +1,144 @@
package wow.doge.http4sdemo
import cats.effect.Sync
import cats.implicits._
import fs2.interop.reactivestreams._
import io.circe.Codec
import io.circe.generic.semiauto._
import monix.bio.Task
import monix.reactive.Observable
import org.http4s.HttpRoutes
import org.http4s.dsl.Http4sDsl
import slick.jdbc.JdbcBackend.DatabaseDef
import slick.jdbc.JdbcProfile
import wow.doge.http4sdemo.dto.Book
import wow.doge.http4sdemo.dto.BookUpdate
import wow.doge.http4sdemo.dto.NewBook
import wow.doge.http4sdemo.services.LibraryService
import wow.doge.http4sdemo.slickcodegen.Tables._
object Http4sdemoRoutes {
def jokeRoutes[F[_]: Sync](J: Jokes[F]): HttpRoutes[F] = {
val dsl = Http4sDsl[F]
import dsl._
HttpRoutes.of[F] { case GET -> Root / "joke" =>
for {
joke <- J.get
resp <- Ok(joke)
} yield resp
}
}
def helloWorldRoutes[F[_]: Sync](H: HelloWorld[F]): HttpRoutes[F] = {
val dsl = new Http4sDsl[F] {}
import dsl._
HttpRoutes.of[F] { case GET -> Root / "hello" / name =>
for {
greeting <- H.hello(HelloWorld.Name(name))
resp <- Ok(greeting)
r2 <- BadRequest("Bad request")
} yield r2
}
}
def userRoutes(userService: UserService): HttpRoutes[Task] = {
val dsl = Http4sDsl[Task]
import dsl._
import org.http4s.circe.CirceEntityCodec._
HttpRoutes.of[Task] { case GET -> Root / "users" =>
Task.deferAction(implicit s =>
for {
_ <- Task.unit
users = userService.users.toReactivePublisher.toStream[Task]
res <- Ok(users)
} yield res
)
}
}
def libraryRoutes(libraryService: LibraryService): HttpRoutes[Task] = {
val dsl = Http4sDsl[Task]
import dsl._
HttpRoutes.of[Task] {
case GET -> Root / "api" / "get" / "books" =>
import org.http4s.circe.streamJsonArrayEncoder
import io.circe.syntax._
Task.deferAction(implicit s =>
for {
books <- Task.pure(
libraryService.getBooks.toReactivePublisher
.toStream[Task]
)
res <- Ok(books.map(_.asJson))
} yield res
)
case GET -> Root / "api" / "get" / "book" / IntVar(id) =>
// import org.http4s.circe.CirceEntityCodec._
import org.http4s.circe.jsonEncoder
import io.circe.syntax._
for {
bookJson <- libraryService.getBookById(id).map(_.asJson)
res <- Ok(bookJson)
} yield res
case req @ POST -> Root / "api" / "post" / "book" =>
import org.http4s.circe.CirceEntityCodec._
for {
newBook <- req.as[NewBook]
book <- libraryService.insertBook(newBook)
res <- Created(book)
} yield res
case req @ PATCH -> Root / "api" / "update" / "book" / IntVar(id) =>
import org.http4s.circe.CirceEntityCodec._
for {
updateData <- req.as[BookUpdate]
_ <- libraryService
.updateBook(id, updateData)
.void
.onErrorHandleWith(ex =>
Task(println(s"Handled -> ${ex.getMessage}"))
)
// .mapError(e => new Exception(e))
res <- Ok()
} yield res
case req @ DELETE -> Root / "api" / "delete" / "book" / IntVar(id) =>
for {
_ <- libraryService.deleteBook(id)
res <- Ok()
} yield res
case req @ POST -> Root / "api" / "post" / "books" / "read" =>
import org.http4s.circe.CirceEntityCodec.circeEntityDecoder
for {
newBook <- req.as[List[Book]]
// book <- libraryService.insertBook(newBook)
res <- Ok("blah")
} yield res
}
}
}
case class User(id: String, email: String)
object User {
val tupled = (this.apply _).tupled
// implicit val decoder: Decoder[User] = deriveDecoder
// implicit def entityDecoder[F[_]: Sync]: EntityDecoder[F, User] =
// jsonOf
// implicit val encoder: Encoder[User] = deriveEncoder
// implicit def entityEncoder[F[_]: Applicative]: EntityEncoder[F, User] =
// jsonEncoderOf
implicit val codec: Codec[User] = deriveCodec
}
class UserService(profile: JdbcProfile, db: DatabaseDef) {
import profile.api._
def users: Observable[User] =
Observable.fromReactivePublisher(
db.stream(Users.map(u => (u.id, u.email).mapTo[User]).result)
)
}

50
src/main/scala/wow/doge/http4sdemo/Http4sdemoServer.scala

@ -0,0 +1,50 @@
package wow.doge.http4sdemo
import cats.implicits._
import fs2.Stream
import monix.bio.Task
import monix.execution.Scheduler
import org.http4s.client.blaze.BlazeClientBuilder
import org.http4s.implicits._
import org.http4s.server.blaze.BlazeServerBuilder
import org.http4s.server.middleware.Logger
import slick.jdbc.JdbcBackend.DatabaseDef
import slick.jdbc.JdbcProfile
import wow.doge.http4sdemo.services.LibraryDbio
import wow.doge.http4sdemo.services.LibraryService
object Http4sdemoServer {
def stream(
db: DatabaseDef,
p: JdbcProfile
)(implicit s: Scheduler): Stream[Task, Nothing] = {
for {
client <- BlazeClientBuilder[Task](s).stream
helloWorldAlg = HelloWorld.impl
jokeAlg = Jokes.impl(client)
ss = new UserService(p, db)
// Combine Service Routes into an HttpApp.
// Can also be done via a Router if you
// want to extract a segments not checked
// in the underlying routes.
libraryDbio = new LibraryDbio(p)
libraryService = new LibraryService(p, libraryDbio, db)
httpApp = (
Http4sdemoRoutes.helloWorldRoutes[Task](helloWorldAlg) <+>
Http4sdemoRoutes.jokeRoutes[Task](jokeAlg) <+>
Http4sdemoRoutes.userRoutes(ss) <+>
Http4sdemoRoutes.libraryRoutes(libraryService)
).orNotFound
// With Middlewares in place
finalHttpApp = Logger.httpApp(true, true)(httpApp)
exitCode <- BlazeServerBuilder[Task](s)
.bindHttp(8081, "0.0.0.0")
.withHttpApp(finalHttpApp)
.serve
} yield exitCode
}.drain
}

47
src/main/scala/wow/doge/http4sdemo/Jokes.scala

@ -0,0 +1,47 @@
package wow.doge.http4sdemo
import cats.Applicative
import cats.effect.Sync
import cats.implicits._
import io.circe.Decoder
import io.circe.Encoder
import io.circe.generic.semiauto._
import monix.bio.Task
import org.http4s.Method._
import org.http4s._
import org.http4s.circe._
import org.http4s.client.Client
import org.http4s.client.dsl.Http4sClientDsl
import org.http4s.implicits._
sealed trait Jokes[F[_]] {
def get: F[Jokes.Joke]
}
object Jokes {
def apply[F[_]](implicit ev: Jokes[F]): Jokes[F] = ev
final case class Joke(joke: String)
object Joke {
implicit val jokeDecoder: Decoder[Joke] = deriveDecoder[Joke]
implicit def jokeEntityDecoder[F[_]: Sync]: EntityDecoder[F, Joke] =
jsonOf
implicit val jokeEncoder: Encoder[Joke] = deriveEncoder[Joke]
implicit def jokeEntityEncoder[F[_]: Applicative]: EntityEncoder[F, Joke] =
jsonEncoderOf
}
final case class JokeError(e: Throwable) extends RuntimeException
def impl(C: Client[Task]): Jokes[Task] = new Jokes[Task] {
val dsl = new Http4sClientDsl[Task] {}
import dsl._
def get: Task[Jokes.Joke] = {
C.expect[Joke](GET(uri"https://icanhazdadjoke.com/"))
.adaptError { case t =>
JokeError(t)
} // Prevent Client Json Decoding Failure Leaking
}
}
}

31
src/main/scala/wow/doge/http4sdemo/Main.scala

@ -0,0 +1,31 @@
package wow.doge.http4sdemo
import cats.effect.ExitCode
import cats.effect.Resource
import monix.bio.BIOApp
import monix.bio.Task
import monix.bio.UIO
import slick.jdbc.JdbcProfile
import wow.doge.http4sdemo.SlickResource
object Main extends BIOApp {
val profile: JdbcProfile = _root_.slick.jdbc.H2Profile
def app = for {
db <- SlickResource("myapp.database")
_ <- Resource.liftF(for {
config <- JdbcDatabaseConfig.loadFromGlobal("myapp.database")
_ <- DBMigrations.migrate(config)
} yield ())
_ <- Resource.liftF(
Task.deferAction(implicit s =>
Http4sdemoServer.stream(db, profile).compile.drain
)
)
} yield ()
def run(args: List[String]) = {
app
.use(_ => Task.never)
.onErrorHandleWith(ex => UIO(ex.printStackTrace()))
.as(ExitCode.Success)
}
}

98
src/main/scala/wow/doge/http4sdemo/Migrate.scala

@ -0,0 +1,98 @@
package wow.doge.http4sdemo
import scala.jdk.CollectionConverters._
import cats.effect.Sync
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import com.typesafe.scalalogging.LazyLogging
import org.flywaydb.core.Flyway
import org.flywaydb.core.api.Location
import org.flywaydb.core.api.configuration.FluentConfiguration
import pureconfig.ConfigConvert
import pureconfig.ConfigSource
import pureconfig.generic.semiauto._
final case class JdbcDatabaseConfig(
url: String,
driver: String,
user: Option[String],
password: Option[String],
migrationsTable: String,
migrationsLocations: List[String]
)
object JdbcDatabaseConfig {
def loadFromGlobal[F[_]: Sync](
configNamespace: String
): F[JdbcDatabaseConfig] =
Sync[F].suspend {
val config = ConfigFactory.load()
load(config.getConfig(configNamespace))
}
// Integration with PureConfig
implicit val configConvert: ConfigConvert[JdbcDatabaseConfig] =
deriveConvert
def load[F[_]: Sync](config: Config): F[JdbcDatabaseConfig] =
Sync[F].delay {
ConfigSource.fromConfig(config).loadOrThrow[JdbcDatabaseConfig]
}
}
object DBMigrations extends LazyLogging {
def migrate[F[_]: Sync](config: JdbcDatabaseConfig): F[Int] =
Sync[F].delay {
logger.info(
"Running migrations from locations: " +
config.migrationsLocations.mkString(", ")
)
val count = unsafeMigrate(config)
logger.info(s"Executed $count migrations")
count
}
private def unsafeMigrate(config: JdbcDatabaseConfig): Int = {
val m: FluentConfiguration = Flyway.configure
.dataSource(
config.url,
config.user.orNull,
config.password.orNull
)
.group(true)
.outOfOrder(false)
.table(config.migrationsTable)
.locations(
config.migrationsLocations
.map(new Location(_))
.toList: _*
)
.baselineOnMigrate(true)
logValidationErrorsIfAny(m)
m.load()
.migrate()
.migrationsExecuted
}
private def logValidationErrorsIfAny(m: FluentConfiguration): Unit = {
val validated = m
.ignorePendingMigrations(true)
.load()
.validateWithResult()
if (!validated.validationSuccessful)
for (error <- validated.invalidMigrations.asScala)
logger.warn(s"""
|Failed validation:
| - version: ${error.version}
| - path: ${error.filepath}
| - description: ${error.description}
| - errorCode: ${error.errorDetails.errorCode}
| - errorMessage: ${error.errorDetails.errorMessage}
""".stripMargin.strip)
}
}

12
src/main/scala/wow/doge/http4sdemo/SlickResource.scala

@ -0,0 +1,12 @@
package wow.doge.http4sdemo
import cats.effect.Resource
import monix.bio.Task
import slick.jdbc.JdbcBackend.Database
object SlickResource {
def apply(confPath: String) =
Resource.make(Task(Database.forConfig(confPath)))(db =>
Task(db.source.close()) >> Task(db.close())
)
}

82
src/main/scala/wow/doge/http4sdemo/dto/Library.scala

@ -0,0 +1,82 @@
package wow.doge.http4sdemo.dto
import java.time.Instant
import io.circe.Printer
import io.circe.generic.semiauto._
import io.scalaland.chimney.dsl._
import org.http4s.EntityEncoder
import org.http4s.circe.streamJsonArrayEncoderWithPrinterOf
import slick.jdbc.JdbcProfile
import wow.doge.http4sdemo.slickcodegen.Tables
final case class Book(
id: Int,
title: String,
authorId: Int,
createdAt: Instant
)
object Book {
def tupled = (Book.apply _).tupled
implicit val ec = deriveCodec[Book]
// implicit def streamEntityEncoder[F[_]]
// : EntityEncoder[F, fs2.Stream[F, Book]] =
// streamJsonArrayEncoderWithPrinterOf(Printer.noSpaces)
def fromBooksRow(row: Tables.BooksRow) = row.transformInto[Book]
def fromBooksTableFn(implicit profile: JdbcProfile) = {
import profile.api._
(b: Tables.Books) => (b.id, b.title, b.authorId, b.createdAt).mapTo[Book]
}
def fromBooksTable(implicit profile: JdbcProfile) =
Tables.Books.map(fromBooksTableFn)
}
final case class NewBook(title: String, authorId: Int)
object NewBook {
def tupled = (NewBook.apply _).tupled
implicit val decoder = deriveDecoder[NewBook]
def fromBooksTable(implicit profile: JdbcProfile) = {
import profile.api._
Tables.Books.map(b => (b.title, b.authorId).mapTo[NewBook])
}
}
final case class BookUpdate(title: Option[String], authorId: Option[Int]) {
import com.softwaremill.quicklens._
def update(row: Tables.BooksRow): Tables.BooksRow =
row
.modify(_.title)
.setToIfDefined(title)
.modify(_.authorId)
.setToIfDefined(authorId)
}
object BookUpdate {
implicit val decoder = deriveDecoder[BookUpdate]
}
final case class Author(id: Int, name: String)
object Author {
def tupled = (Author.apply _).tupled
implicit val codec = deriveCodec[Author]
implicit def streamEntityEncoder[F[_]]
: EntityEncoder[F, fs2.Stream[F, Author]] =
streamJsonArrayEncoderWithPrinterOf(Printer.noSpaces)
}
final case class NewAuthor(name: String)
final case class BookWithAuthor(
id: Int,
title: String,
author: Author,
createdAt: Instant
)
object BookWithAuthor {
def tupled = (BookWithAuthor.apply _).tupled
implicit val codec = deriveCodec[BookWithAuthor]
implicit def streamEntityEncoder[F[_]]
: EntityEncoder[F, fs2.Stream[F, BookWithAuthor]] =
streamJsonArrayEncoderWithPrinterOf(Printer.noSpaces)
}

3
src/main/scala/wow/doge/http4sdemo/dto/User.scala

@ -0,0 +1,3 @@
package wow.doge.http4sdemo.dto
final case class NewUser(email: String)

31
src/main/scala/wow/doge/http4sdemo/implicits/package.scala

@ -0,0 +1,31 @@
package wow.doge.http4sdemo
import monix.bio.IO
import monix.bio.Task
import monix.reactive.Observable
import slick.dbio.DBIOAction
import slick.dbio.NoStream
import slick.dbio.Streaming
import slick.jdbc.JdbcBackend.DatabaseDef
package object implicits {
implicit class DatabaseDefExt(private val db: DatabaseDef) extends AnyVal {
def runL[R](a: DBIOAction[R, NoStream, Nothing]) =
Task.deferFuture(db.run(a))
def streamO[T](a: DBIOAction[_, Streaming[T], Nothing]) =
Observable.fromReactivePublisher(db.stream(a))
}
implicit final class MonixEvalTaskExt[T](private val task: monix.eval.Task[T])
extends AnyVal {
def toIO = IO.deferAction(implicit s => IO.from(task))
}
implicit final class MonixBioTaskExt[T](private val task: monix.bio.Task[T])
extends AnyVal {
def toTask =
monix.eval.Task.deferAction(implicit s => monix.eval.Task.from(task))
}
}

103
src/main/scala/wow/doge/http4sdemo/services/LibraryService.scala

@ -0,0 +1,103 @@
package wow.doge.http4sdemo.services
import monix.bio.IO
import monix.bio.Task
import slick.jdbc.JdbcBackend
import slick.jdbc.JdbcProfile
import wow.doge.http4sdemo.dto.Book
import wow.doge.http4sdemo.dto.BookUpdate
import wow.doge.http4sdemo.dto.NewBook
import wow.doge.http4sdemo.implicits._
import wow.doge.http4sdemo.slickcodegen.Tables
class LibraryService(
profile: JdbcProfile,
dbio: LibraryDbio,
db: JdbcBackend.DatabaseDef
) {
import profile.api._
def getBooks = db.streamO(dbio.getBooks)
def getBookById(id: Int) = db.runL(dbio.getBook(id))
// .map(b =>
// (b.title, b.authorId, b.createdAt).mapTo[BookUpdateEntity]
// )
def updateBook(id: Int, updateData: BookUpdate) =
for {
action <- IO.deferAction { implicit s =>
Task(for {
mbRow <- dbio.selectBook(id).result.headOption
updatedRow <- mbRow match {
case Some(value) =>
println(s"Original value -> $value")
println(s"Value to be updated with -> $updateData")
DBIO.successful(updateData.update(value))
case None =>
DBIO.failed(new Exception(s"Book with id $id does not exist"))
}
updateAction = dbio.selectBook(id).update(updatedRow)
_ = println(s"SQL = ${updateAction.statements}")
_ <- updateAction
} yield ())
}
_ <- db.runL(action.transactionally.asTry).flatMap(Task.fromTry)
} yield ()
def deleteBook(id: Int) = db.runL(dbio.deleteBook(id))
def insertBook(newBook: NewBook) =
Task.deferFutureAction { implicit s =>
val action = for {
id <- dbio.insertBookAndGetId(newBook)
book <- dbio.getBook(id)
} yield book.get
db.run(action.transactionally)
}
def booksForAuthor(authorId: Int) =
db.streamO(dbio.booksForAuthor(authorId)).map(Book.fromBooksRow)
}
class LibraryDbio(val profile: JdbcProfile) {
import profile.api._
def getBooks: StreamingDBIO[Seq[Book], Book] = Query.getBooksInner.result
def insertBookAndGetId(newBook: NewBook): DBIO[Int] =
Query.insertBookGetId += newBook
def insertBookAndGetBook(newBook: NewBook): DBIO[Book] =
Query.insertBookGetBook += newBook
def selectBook(id: Int) = Tables.Books.filter(_.id === id)
def deleteBook(id: Int) = selectBook(id).delete
def getBook(id: Int) = selectBook(id)
.map(Book.fromBooksTableFn)
.result
.headOption
def booksForAuthor(authorId: Int) = Query.booksForAuthorInner(authorId).result
private object Query {
val getBooksInner = Book.fromBooksTable
val insertBookGetId =
NewBook.fromBooksTable.returning(Tables.Books.map(_.id))
val insertBookGetBook = NewBook.fromBooksTable.returning(getBooksInner)
def booksForAuthorInner(authorId: Int) = for {
b <- Tables.Books
a <- selectAuthor(authorId) if b.authorId === a.id
} yield b
def selectAuthor(authorId: Int) = Tables.Authors.filter(_.id === authorId)
}
}

25
src/test/scala/wow/doge/http4sdemo/HelloWorldSpec.scala

@ -0,0 +1,25 @@
package wow.doge.http4sdemo
import cats.effect.IO
import org.http4s._
import org.http4s.implicits._
import munit.CatsEffectSuite
class HelloWorldSpec extends CatsEffectSuite {
// test("HelloWorld returns status code 200") {
// assertIO(retHelloWorld.map(_.status), Status.Ok)
// }
// test("HelloWorld returns hello world message") {
// assertIO(
// retHelloWorld.flatMap(_.as[String]),
// "{\"message\":\"Hello, world\"}"
// )
// }
// private[this] val retHelloWorld: IO[Response[IO]] = {
// val getHW = Request[IO](Method.GET, uri"/hello/world")
// val helloWorld = HelloWorld.impl[IO]
// Http4sdemoRoutes.helloWorldRoutes(helloWorld).orNotFound(getHW)
// }
}
Loading…
Cancel
Save