first commit
This commit is contained in:
commit
c702bb0773
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
@ -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
Normal file
1
.scalafmt.conf
Normal file
@ -0,0 +1 @@
|
||||
version = "2.7.4"
|
175
build.sbt
Normal file
175
build.sbt
Normal file
@ -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);
|
6
native-image-readme.md
Normal file
6
native-image-readme.md
Normal file
@ -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
Normal file
2
project/build.properties
Normal file
@ -0,0 +1,2 @@
|
||||
sbt.version=1.4.7
|
||||
|
12
project/plugins.sbt
Normal file
12
project/plugins.sbt
Normal file
@ -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
|
||||
}
|
||||
]
|
38
src/main/resources/application.conf
Normal file
38
src/main/resources/application.conf
Normal file
@ -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
Normal file
16
src/main/resources/logback.xml
Normal file
@ -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
Normal file
40
src/main/scala/wow/doge/http4sdemo/HelloWorld.scala
Normal file
@ -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
Normal file
144
src/main/scala/wow/doge/http4sdemo/Http4sdemoRoutes.scala
Normal file
@ -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
Normal file
50
src/main/scala/wow/doge/http4sdemo/Http4sdemoServer.scala
Normal file
@ -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
Normal file
47
src/main/scala/wow/doge/http4sdemo/Jokes.scala
Normal file
@ -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
Normal file
31
src/main/scala/wow/doge/http4sdemo/Main.scala
Normal file
@ -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
Normal file
98
src/main/scala/wow/doge/http4sdemo/Migrate.scala
Normal file
@ -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
Normal file
12
src/main/scala/wow/doge/http4sdemo/SlickResource.scala
Normal file
@ -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
Normal file
82
src/main/scala/wow/doge/http4sdemo/dto/Library.scala
Normal file
@ -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
Normal file
3
src/main/scala/wow/doge/http4sdemo/dto/User.scala
Normal file
@ -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
Normal file
31
src/main/scala/wow/doge/http4sdemo/implicits/package.scala
Normal file
@ -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
Normal file
103
src/main/scala/wow/doge/http4sdemo/services/LibraryService.scala
Normal file
@ -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
Normal file
25
src/test/scala/wow/doge/http4sdemo/HelloWorldSpec.scala
Normal file
@ -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…
Reference in New Issue
Block a user