Rohan Sircar
3 years ago
commit
c702bb0773
27 changed files with 1175 additions and 0 deletions
-
29.gitignore
-
1.scalafmt.conf
-
175build.sbt
-
6modules/flyway/src/main/resources/db/migration/default/V1__create_users_table.sql
-
6modules/flyway/src/main/resources/db/migration/default/V2__add_user.sql
-
6modules/flyway/src/main/resources/db/migration/default/V3__create_cars_table.sql
-
6modules/flyway/src/main/resources/db/migration/default/V4__add_car.sql
-
12modules/flyway/src/main/resources/db/migration/default/V5__authors_books_table.sql
-
14modules/flyway/src/main/resources/db/migration/default/V6__insert_books_and_authors.sql
-
6native-image-readme.md
-
2project/build.properties
-
12project/plugins.sbt
-
180src/main/resources/META-INF/native-image/wow/doge/http4sdemo/reflect-config.json
-
38src/main/resources/application.conf
-
16src/main/resources/logback.xml
-
40src/main/scala/wow/doge/http4sdemo/HelloWorld.scala
-
144src/main/scala/wow/doge/http4sdemo/Http4sdemoRoutes.scala
-
50src/main/scala/wow/doge/http4sdemo/Http4sdemoServer.scala
-
47src/main/scala/wow/doge/http4sdemo/Jokes.scala
-
31src/main/scala/wow/doge/http4sdemo/Main.scala
-
98src/main/scala/wow/doge/http4sdemo/Migrate.scala
-
12src/main/scala/wow/doge/http4sdemo/SlickResource.scala
-
82src/main/scala/wow/doge/http4sdemo/dto/Library.scala
-
3src/main/scala/wow/doge/http4sdemo/dto/User.scala
-
31src/main/scala/wow/doge/http4sdemo/implicits/package.scala
-
103src/main/scala/wow/doge/http4sdemo/services/LibraryService.scala
-
25src/test/scala/wow/doge/http4sdemo/HelloWorldSpec.scala
@ -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 |
@ -0,0 +1 @@ |
|||
version = "2.7.4" |
@ -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) |
@ -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 |
|||
); |
@ -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') |
|||
); |
@ -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 |
|||
); |
@ -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') |
|||
); |
@ -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 |
|||
); |
@ -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); |
@ -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 |
|||
``` |
|||
|
@ -0,0 +1,2 @@ |
|||
sbt.version=1.4.7 |
|||
|
@ -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") |
@ -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 |
|||
} |
|||
] |
@ -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" |
|||
] |
|||
} |
|||
} |
|||
|
|||
|
@ -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> |
@ -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] |
|||
} |
|||
} |
@ -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) |
|||
) |
|||
|
|||
} |
@ -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 |
|||
} |
@ -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 |
|||
} |
|||
} |
|||
|
|||
} |
@ -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) |
|||
} |
|||
} |
@ -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) |
|||
} |
|||
} |
@ -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()) |
|||
) |
|||
} |
@ -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) |
|||
} |
@ -0,0 +1,3 @@ |
|||
package wow.doge.http4sdemo.dto |
|||
|
|||
final case class NewUser(email: String) |
@ -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)) |
|||
} |
|||
|
|||
} |
@ -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) |
|||
} |
|||
} |
@ -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) |
|||
// } |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue