diff --git a/.scalafmt.conf b/.scalafmt.conf
index 85a2f090..5ab2398e 100644
--- a/.scalafmt.conf
+++ b/.scalafmt.conf
@@ -1,10 +1,13 @@
-version=2.7.5
+version = 3.8.2
+runner.dialect = Scala3
+runner.dialectOverride.allowSignificantIndentation = false
+runner.dialectOverride.allowQuietSyntax = true
maxColumn = 120
style = IntelliJ
align = most
align.openParenCallSite = true
-align.tokens.add = ["=",":"]
+align.tokens."+" = ["=", ":"]
assumeStandardLibraryStripMargin = true
newlines.sometimesBeforeColonInMethodReturnType = false
optIn.breakChainOnFirstMethodDot = false
diff --git a/.sdkmanrc b/.sdkmanrc
index 79ffbd7b..df7563ff 100644
--- a/.sdkmanrc
+++ b/.sdkmanrc
@@ -1,3 +1,3 @@
# Enable auto-env through the sdkman_auto_env config
# Add key=value pairs of SDKs to use below
-java=22.1.0.r17-grl
+java=22.0.2-graalce
diff --git a/Justfile b/Justfile
new file mode 100644
index 00000000..1613ee57
--- /dev/null
+++ b/Justfile
@@ -0,0 +1,53 @@
+# common variables
+
+set shell := ["zsh", "--login", "-c"]
+
+network-prefix := `basename "$PWD"`
+config-dir := justfile_directory() + "/docker-compose"
+dev-configs := "--file " + config-dir + "/monolith-deps.yml --file " + config-dir + "/monolith-setup.yml"
+local-configs := dev-configs + " --file " + config-dir + "/monolith-app.yml"
+
+# fetch dependencies
+pull:
+ docker compose --project-directory . {{dev-configs}} pull
+
+# dev environment to use with sbt
+dev-bg:
+ docker compose --project-directory . {{dev-configs}} up --detach --quiet-pull
+dev-up:
+ docker compose --project-directory . {{dev-configs}} up
+dev-stop:
+ docker compose --project-directory . {{dev-configs}} stop
+dev-down:
+ docker-compose --project-directory . {{dev-configs}} down --remove-orphans || \
+ (docker container rm {{network-prefix}}_kafka_1 {{network-prefix}}_postgres_1 -f && \
+ docker network disconnect {{network-prefix}}_branchtalk-monolith {{network-prefix}}_kafka_1 -f && \
+ docker network disconnect {{network-prefix}}_branchtalk-monolith {{network-prefix}}_postgres_1 -f && \
+ docker network rm {{network-prefix}}_branchtalk-monolith)
+dev-ps:
+ docker compose --project-directory . {{dev-configs}} ps
+dev-logs:
+ docker compose --project-directory . {{dev-configs}} logs -f ${LOGS}
+
+# whole monolithic app setup for e.g. local frontend development
+local-bg:
+ (docker compose --project-directory . {{local-configs}} up --detach --quiet-pull) || (echo "publish application with sbt application/docker:publishLocal!")
+local-up:
+ (docker compose --project-directory . {{local-configs}} up) || (echo "publish application with sbt application/docker:publishLocal!")
+local-stop:
+ docker compose --project-directory . {{local-configs}} stop
+local-down:
+ docker compose --project-directory . {{local-configs}} down --remove-orphans || \
+ (docker container rm {{network-prefix}}_kafka_1 {{network-prefix}}_postgres_1 -f && \
+ docker network disconnect {{network-prefix}}_branchtalk-monolith {{network-prefix}}_kafka_1 -f && \
+ docker network disconnect {{network-prefix}}_branchtalk-monolith {{network-prefix}}_postgres_1 -f && \
+ docker network rm {{network-prefix}}_branchtalk-monolith)
+local-ps:
+ docker compose --project-directory . {{local-configs}} ps
+local-logs:
+ docker compose --project-directory . {{local-configs}} logs -f ${LOGS}
+
+clean-volumes:
+ docker volume rm {{network-prefix}}_postgres_data -f
+ docker volume rm {{network-prefix}}_kafka_data -f
+ docker volume rm {{network-prefix}}_zookeeper_data -f
diff --git a/branchtalk.sbt b/branchtalk.sbt
index 848fc05a..a1d2c332 100644
--- a/branchtalk.sbt
+++ b/branchtalk.sbt
@@ -1,301 +1,461 @@
-import sbt._
-import Settings._
-import sbtcrossproject.CrossPlugin.autoImport.{ CrossType, crossProject }
+import sbt.*
+import commandmatrix.extra.*
import com.typesafe.sbt.SbtNativePackager.Docker
+import org.scalafmt.sbt.ScalafmtPlugin.scalafmtConfigSettings
-Global / excludeLintKeys ++= Set(scalacOptions, trapExit)
+Global / excludeLintKeys ++= Set(
+ dockerExposedPorts,
+ dockerUpdateLatest,
+ fork,
+ ideSkipProject,
+ libraryDependencies,
+ packageName,
+ scalacOptions,
+ trapExit
+)
-lazy val root = project.root
- .setName("branchtalk")
- .setDescription("branchtalk build")
- .configureRoot
- .aggregate(
- commonJVM,
- commonInfrastructure,
- commonApiJVM,
- discussionsJVM,
- discussionsApiJVM,
- discussionsImpl,
- usersJVM,
- usersApiJVM,
- usersImpl,
- server,
- application
+// Common settings:
+
+val only1VersionInIDE = Seq(
+ MatrixAction.ForPlatform(VirtualAxis.jvm).Configure(_.settings(ideSkipProject := false, bspEnabled := true)),
+ MatrixAction.ForPlatform(VirtualAxis.js).Configure(_.settings(ideSkipProject := true, bspEnabled := false))
+)
+
+val settings = Seq(
+ organization := "io.branchtalk",
+ scalaVersion := Dependencies.scalaVersion,
+ scalacOptions ++= Seq(
+ // standard settings
+ // format: off
+ "-encoding", "UTF-8",
+ "-rewrite",
+ "-source", "3.3-migration",
+ // format: on
+ "-unchecked",
+ "-deprecation",
+ // "-explaintypes",
+ "-feature",
+ "-no-indent",
+ // format: off
+ "-Xmax-inlines", "64",
+ // format: on
+ "-Wconf:msg=The method `apply` is inserted:s",
+ "-Wnonunit-statement",
+ "-Wvalue-discard",
+ // "-Xfatal-warnings",
+ "-Ykind-projector:underscores"
+ ),
+ console / scalacOptions --= Seq(
+ // warnings
+ "-Ywarn-unused",
+ // "-Wunused:imports", // import x.Underlying as X is marked as unused even though it is! probably one of https://github.com/scala/scala3/issues/: #18564, #19252, #19657, #19912
+ "-Wunused:privates",
+ "-Wunused:locals",
+ "-Wunused:explicits",
+ "-Wunused:implicits",
+ "-Wunused:params",
+ "-Wvalue-discard",
+ "-Xfatal-warnings",
+ "-Xcheck-macros",
+ // advanced options
+ "-Xfatal-warnings",
+ // linting
+ "-Xlint"
+ ),
+ Global / cancelable := true,
+ Compile / trapExit := false,
+ Compile / connectInput := true,
+ Compile / outputStrategy := Some(StdoutOutput),
+ libraryDependencies ++= Seq(
+ Dependencies.cats,
+ Dependencies.catsFree,
+ Dependencies.catsEffect,
+ Dependencies.alleycats,
+ Dependencies.kittens,
+ Dependencies.chimney,
+ Dependencies.enumeratum,
+ Dependencies.fastuuid,
+ Dependencies.uuidGenerator,
+ Dependencies.log4cats,
+ Dependencies.log4catsSlf4j,
+ Dependencies.magnolia,
+ Dependencies.neotype,
+ Dependencies.scalaLogging,
+ Dependencies.quicklens,
+ Dependencies.logback
+ ),
+ Compile / scalafmtOnCompile := true,
+ Compile / compile / wartremoverWarnings ++= Warts.allBut(
+ Wart.Any,
+ Wart.DefaultArguments,
+ Wart.ExplicitImplicitTypes,
+ Wart.ImplicitConversion,
+ Wart.ImplicitParameter,
+ Wart.Overloading,
+ Wart.PublicInference,
+ Wart.NonUnitStatements,
+ Wart.Nothing
+ ),
+ // used A LOT in Specs2
+ Test / scalacOptions ++= Seq(
+ "-Wconf:msg=unused value of type:s",
+ "-language:implicitConversions"
+ ),
+ // don't publish
+ publish / skip := true,
+ publishArtifact := false
+)
+
+val tests = Seq(
+ libraryDependencies ++= Seq(
+ Dependencies.catsLaws % Test,
+ Dependencies.spec2Core % Test,
+ Dependencies.spec2Scalacheck % Test
+ )
+) ++ inConfig(Test)(
+ Seq(
+ scalafmtOnCompile := true,
+ testFrameworks := Seq(TestFrameworks.Specs2),
+ libraryDependencies ++= Seq(
+ Dependencies.catsLaws % Test,
+ Dependencies.spec2Core % Test,
+ Dependencies.spec2Scalacheck % Test
+ )
)
+) ++ inConfig(Test)(scalafmtConfigSettings)
-lazy val scalaJsArtifacts = project.in(file("scala-js"))
- .setName("scala-js-artifacts")
- .setDescription("aggregates all Scala.js modules to publish")
- .configureRoot
- .aggregate(
- commonMacrosJS,
- commonJS,
- commonApiMacrosJS,
- commonApiJS,
- discussionsJS,
- discussionsApiJS,
- usersJS,
- usersApiJS
+val integrationTests = tests ++ Seq(
+ Test / fork := true
+)
+
+def customPredef(imports: String*): Def.Setting[Task[Seq[String]]] =
+ scalacOptions += s"-Yimports:${(Seq("java.lang", "scala", "scala.Predef") ++ imports).mkString(",")}"
+
+// modules
+
+lazy val root = project
+ .in(file("."))
+ .enablePlugins(GitVersioning, GitBranchPrompt)
+ .settings(
+ name := "branchtalk",
+ description := "branchtalk build"
)
+ .settings(settings *)
+ .aggregate(common.projectRefs *)
+ .aggregate(commonApi.projectRefs *)
+ .aggregate(commonInfrastructure)
+ .aggregate(discussions.projectRefs *)
+ .aggregate(discussionsApi.projectRefs *)
+ .aggregate(users.projectRefs *)
+ .aggregate(usersApi.projectRefs *)
+ .aggregate(usersImpl)
+ .aggregate(server, application)
-addCommandAlias("fmt", ";scalafmt;Test/scalafmt;It/scalafmt")
-addCommandAlias("fullTest", ";test;It/test")
-addCommandAlias("fullCoverageTest", ";coverage;test;It/test;coverageReport;coverageAggregate")
+lazy val scalaJsArtifacts = project
+ .in(file("scala-js"))
+ .enablePlugins(GitVersioning, GitBranchPrompt)
+ .settings(
+ name := "scala-js-artifacts",
+ description := "aggregates all Scala.js modules to publish"
+ )
+ .settings(settings *)
+ .aggregate(
+ common.js(Dependencies.scalaVersion),
+ commonApi.js(Dependencies.scalaVersion),
+ discussions.js(Dependencies.scalaVersion),
+ discussionsApi.js(Dependencies.scalaVersion),
+ users.js(Dependencies.scalaVersion),
+ usersApi.js(Dependencies.scalaVersion)
+ )
// commons
-val commonMacros =
- crossProject(JVMPlatform, JSPlatform)
- .crossType(CrossType.Pure)
- .build
- .from("common-macros")
- .setName("common-macros")
- .setDescription("Common macro definitions")
- .configureModule
-val commonMacrosJVM = commonMacros.jvm
-val commonMacrosJS = commonMacros.js
-
-val common = crossProject(JVMPlatform, JSPlatform)
- .crossType(CrossType.Pure)
- .build
- .from("common")
- .setName("common")
- .setDescription("Common utilities")
- .configureModule
+val common = projectMatrix
+ .in(file("modules/common"))
+ .someVariations(List(Dependencies.scalaVersion), List(VirtualAxis.jvm, VirtualAxis.js))(only1VersionInIDE *)
+ .defaultAxes(VirtualAxis.scalaABIVersion(Dependencies.scalaVersion), VirtualAxis.jvm)
+ .enablePlugins(GitVersioning, GitBranchPrompt)
+ .settings(
+ name := "common",
+ description := "Common utilities"
+ )
+ .settings(settings *)
+ .settings(tests *)
.settings(
libraryDependencies ++= Seq(
Dependencies.avro4s,
- Dependencies.avro4sRefined,
- Dependencies.catnip,
+ Dependencies.avro4sCats,
Dependencies.sourcecode,
Dependencies.jfairy % Test,
Dependencies.guice % Test, // required by jfairy on JDK 15+
Dependencies.guiceAssisted % Test // required by jfairy on JDK 15+
),
- customPredef("scala.util.chaining", "cats.implicits", "eu.timepit.refined.auto")
+ customPredef("scala.util.chaining", "cats.implicits")
)
+
+val commonApi = projectMatrix
+ .someVariations(List(Dependencies.scalaVersion), List(VirtualAxis.jvm, VirtualAxis.js))(only1VersionInIDE *)
+ .defaultAxes(VirtualAxis.scalaABIVersion(Dependencies.scalaVersion), VirtualAxis.jvm)
+ .enablePlugins(GitVersioning, GitBranchPrompt)
+ .in(file("modules/common-api"))
.settings(
- Compile / resourceGenerators += task[Seq[File]] {
- val file = (Compile / resourceManaged).value / "branchtalk-version.conf"
- IO.write(
- file,
- s"""# Populated by the build tool, used by e.g. OpenAPI to display version.
- |branchtalk-build {
- | version = "${version.value}"
- | commit = "${git.gitHeadCommit.value.getOrElse("null")}"
- | date = "${git.gitHeadCommitDate.value.getOrElse("null")}"
- |}""".stripMargin
- )
- Seq(file)
- }
+ name := "common-api",
+ description := "Infrastructure-dependent implementations"
+ )
+ .settings(settings *)
+ .settings(tests *)
+ .settings(
+ libraryDependencies ++= Seq(
+ Dependencies.jsoniter,
+ Dependencies.jsoniterMacro,
+ Dependencies.neotypeTapir,
+ Dependencies.neotypeJsoniter,
+ Dependencies.tapir,
+ Dependencies.tapirJsoniter
+ ),
+ customPredef("scala.util.chaining", "cats.implicits", "neotype")
)
- .dependsOn(commonMacros)
-val commonJVM = common.jvm
-val commonJS = common.js
+ .dependsOn(common)
val commonInfrastructure = project
- .from("common-infrastructure")
- .setName("common-infrastructure")
- .setDescription("Infrastructure-dependent implementations")
- .configureModule
- .configureIntegrationTests()
+ .in(file("modules/common-infrastructure"))
+ .enablePlugins(GitVersioning, GitBranchPrompt)
+ .settings(
+ name := "common-infrastructure",
+ description := "Infrastructure-dependent implementations"
+ )
+ .settings(settings *)
+ .settings(integrationTests *)
.settings(
libraryDependencies ++= Seq(
Dependencies.doobie,
Dependencies.doobieHikari,
Dependencies.doobiePostgres,
- Dependencies.doobieRefined,
+ Dependencies.enumeratumDoobie,
Dependencies.flyway,
+ Dependencies.flywayPostgres,
Dependencies.fs2,
Dependencies.fs2IO,
Dependencies.fs2Kafka,
+ Dependencies.neotypeDoobie,
Dependencies.prometheus,
Dependencies.pureConfig,
Dependencies.pureConfigCats,
Dependencies.pureConfigEnumeratum,
- Dependencies.redis4cats,
- Dependencies.refinedPureConfig
+ Dependencies.redis4cats
),
- customPredef("scala.util.chaining", "cats.implicits", "eu.timepit.refined.auto")
+ customPredef("scala.util.chaining", "cats.implicits", "neotype")
)
- .dependsOn(commonJVM)
+ .dependsOn(common.jvm(Dependencies.scalaVersion))
-val commonApiMacros = crossProject(JVMPlatform, JSPlatform)
- .crossType(CrossType.Pure)
- .build
- .from("common-api-macros")
- .setName("common-api-macros")
- .setDescription("Common API macro definitions")
- .configureModule
-val commonApiMacrosJVM = commonApiMacros.jvm
-val commonApiMacrosJS = commonApiMacros.js
+// discussions
-val commonApi = crossProject(JVMPlatform, JSPlatform)
- .crossType(CrossType.Pure)
- .build
- .from("common-api")
- .setName("common-api")
- .setDescription("Infrastructure-dependent implementations")
- .configureModule
- .configureTests()
+val discussions = projectMatrix
+ .someVariations(List(Dependencies.scalaVersion), List(VirtualAxis.jvm, VirtualAxis.js))(only1VersionInIDE *)
+ .defaultAxes(VirtualAxis.scalaABIVersion(Dependencies.scalaVersion), VirtualAxis.jvm)
+ .enablePlugins(GitVersioning, GitBranchPrompt)
+ .in(file("modules/discussions"))
.settings(
- libraryDependencies ++= Seq(
- Dependencies.jsoniter,
- Dependencies.jsoniterMacro,
- Dependencies.tapir,
- Dependencies.tapirJsoniter,
- Dependencies.tapirRefined
- ),
- customPredef("scala.util.chaining", "cats.implicits", "eu.timepit.refined.auto")
+ name := "discussions",
+ description := "Discussions' published language"
)
- .dependsOn(common, commonApiMacros)
-val commonApiJVM = commonApi.jvm
-val commonApiJS = commonApi.js
-
-// discussions
-
-val discussions = crossProject(JVMPlatform, JSPlatform)
- .crossType(CrossType.Pure)
- .build
- .from("discussions")
- .setName("discussions")
- .setDescription("Discussions' published language")
- .configureModule
- .configureTests()
+ .settings(settings)
+ .settings(tests)
.settings(
- customPredef("scala.util.chaining", "cats.implicits", "eu.timepit.refined.auto")
+ customPredef("scala.util.chaining", "cats.implicits", "neotype")
)
.dependsOn(common)
-val discussionsJVM = discussions.jvm
-val discussionsJS = discussions.js
-val discussionsApi = crossProject(JVMPlatform, JSPlatform)
- .crossType(CrossType.Pure)
- .build
- .from("discussions-api")
- .setName("discussions-api")
- .setDescription("Discussions' HTTP API")
- .configureModule
- .configureTests()
+val discussionsApi = projectMatrix
+ .someVariations(List(Dependencies.scalaVersion), List(VirtualAxis.jvm, VirtualAxis.js))(only1VersionInIDE *)
+ .defaultAxes(VirtualAxis.scalaABIVersion(Dependencies.scalaVersion), VirtualAxis.jvm)
+ .enablePlugins(GitVersioning, GitBranchPrompt)
+ .in(file("modules/discussions-api"))
+ .settings(
+ name := "discussions-api",
+ description := "Discussions' HTTP API"
+ )
+ .settings(settings)
+ .settings(tests)
.settings(
libraryDependencies ++= Seq(
Dependencies.jsoniterMacro
),
- customPredef("scala.util.chaining", "cats.implicits", "eu.timepit.refined.auto")
+ customPredef("scala.util.chaining", "cats.implicits", "neotype")
)
.dependsOn(commonApi, discussions)
-val discussionsApiJVM = discussionsApi.jvm
-val discussionsApiJS = discussionsApi.js
val discussionsImpl = project
- .from("discussions-impl")
- .setName("discussions-impl")
- .setDescription("Discussions' Reads, Writes and Services' implementations")
- .configureModule
- .configureIntegrationTests(requiresFork = true)
+ .in(file("modules/discussions-impl"))
+ .enablePlugins(GitVersioning, GitBranchPrompt)
.settings(
- libraryDependencies += Dependencies.macwire,
- customPredef("scala.util.chaining", "cats.implicits", "eu.timepit.refined.auto")
+ name := "discussions-impl",
+ description := "Discussions' Reads, Writes and Services' implementations"
+ )
+ .settings(settings)
+ .settings(integrationTests)
+ .settings(
+ customPredef("scala.util.chaining", "cats.implicits", "neotype")
+ )
+ .dependsOn(
+ common.jvm(Dependencies.scalaVersion) % s"$Compile->$Compile ; $Test->$Test",
+ commonInfrastructure % s"$Compile->$Compile ; $Test->$Test",
+ discussions.jvm(Dependencies.scalaVersion)
)
- .compileAndTestDependsOn(commonInfrastructure)
- .dependsOn(discussionsJVM, commonJVM % "compile->compile;it->test")
// users
-val users = crossProject(JVMPlatform, JSPlatform)
- .crossType(CrossType.Pure)
- .build
- .from("users")
- .setName("users")
- .setDescription("Users' published language")
- .configureModule
- .configureTests()
+val users = projectMatrix
+ .in(file("modules/users"))
+ .someVariations(List(Dependencies.scalaVersion), List(VirtualAxis.jvm, VirtualAxis.js))(only1VersionInIDE *)
+ .defaultAxes(VirtualAxis.scalaABIVersion(Dependencies.scalaVersion), VirtualAxis.jvm)
+ .enablePlugins(GitVersioning, GitBranchPrompt)
+ .settings(
+ name := "users",
+ description := "Users' published language"
+ )
+ .settings(settings)
+ .settings(tests)
.settings(
libraryDependencies ++= Seq(
Dependencies.bcrypt
),
- customPredef("scala.util.chaining", "cats.implicits", "eu.timepit.refined.auto")
+ customPredef("scala.util.chaining", "cats.implicits", "neotype")
)
.dependsOn(common)
-val usersJVM = users.jvm
-val usersJS = users.js
-val usersApi = crossProject(JVMPlatform, JSPlatform)
- .crossType(CrossType.Pure)
- .build
- .from("users-api")
- .setName("users-api")
- .setDescription("Users' HTTP API")
- .configureModule
- .configureTests()
+val usersApi = projectMatrix
+ .in(file("modules/users-api"))
+ .someVariations(List(Dependencies.scalaVersion), List(VirtualAxis.jvm, VirtualAxis.js))(only1VersionInIDE *)
+ .defaultAxes(VirtualAxis.scalaABIVersion(Dependencies.scalaVersion), VirtualAxis.jvm)
+ .enablePlugins(GitVersioning, GitBranchPrompt)
+ .settings(
+ name := "users-api",
+ description := "Users' HTTP API"
+ )
+ .settings(settings)
+ .settings(tests)
.settings(
libraryDependencies ++= Seq(
Dependencies.jsoniterMacro
),
- customPredef("scala.util.chaining", "cats.implicits", "eu.timepit.refined.auto")
+ customPredef("scala.util.chaining", "cats.implicits", "neotype")
)
.dependsOn(commonApi, users)
-val usersApiJVM = usersApi.jvm
-val usersApiJS = usersApi.js
val usersImpl = project
- .from("users-impl")
- .setName("users-impl")
- .setDescription("Users' Reads, Writes and Services' implementations")
- .configureModule
- .configureIntegrationTests(requiresFork = true)
+ .in(file("modules/users-impl"))
+ .enablePlugins(GitVersioning, GitBranchPrompt)
+ .settings(
+ name := "users-impl",
+ description := "Users' Reads, Writes and Services' implementations"
+ )
+ .settings(settings)
+ .settings(integrationTests)
.settings(
libraryDependencies ++= Seq(
Dependencies.jsoniter,
- Dependencies.jsoniterMacro,
- Dependencies.macwire
+ Dependencies.jsoniterMacro
),
- customPredef("scala.util.chaining", "cats.implicits", "eu.timepit.refined.auto")
+ customPredef("scala.util.chaining", "cats.implicits", "neotype")
+ )
+ .dependsOn(
+ common.jvm(Dependencies.scalaVersion) % s"$Compile->$Compile ; $Test->$Test",
+ commonInfrastructure % s"$Compile->$Compile ; $Test->$Test",
+ discussions.jvm(Dependencies.scalaVersion),
+ users.jvm(Dependencies.scalaVersion)
)
- .compileAndTestDependsOn(commonInfrastructure)
- .dependsOn(usersJVM, discussionsJVM, commonJVM % "compile->compile;it->test")
// application
val server = project
- .from("server")
- .setName("server")
- .setDescription("Branchtalk backend business logic")
- .configureModule
- .configureIntegrationTests(requiresFork = true)
+ .in(file("modules/server"))
+ .enablePlugins(GitVersioning, GitBranchPrompt)
+ .settings(
+ name := "server",
+ description := "Branchtalk backend business logic"
+ )
+ .settings(settings)
+ .settings(integrationTests)
.settings(
libraryDependencies ++= Seq(
Dependencies.decline,
- Dependencies.refinedDecline,
Dependencies.jsoniterMacro,
- Dependencies.sttpCats % IntegrationTest,
+ Dependencies.sttpCats % Test,
Dependencies.http4sBlaze,
Dependencies.http4sPrometheus,
Dependencies.tapirHttp4s,
Dependencies.tapirOpenAPI,
Dependencies.tapirSwaggerUI,
- Dependencies.tapirSTTP % IntegrationTest,
- Dependencies.macwire
+ Dependencies.tapirSTTP % Test
),
- customPredef("scala.util.chaining", "cats.implicits", "eu.timepit.refined.auto")
+ customPredef("scala.util.chaining", "cats.implicits", "neotype"),
+ Compile / resourceGenerators += task[Seq[File]] {
+ val file = (Compile / resourceManaged).value / "branchtalk-version.conf"
+ IO.write(
+ file,
+ s"""# Populated by the build tool, used by e.g. OpenAPI to display version.
+ |branchtalk-build {
+ | version = "${version.value}"
+ | commit = "${git.gitHeadCommit.value.getOrElse("null")}"
+ | date = "${git.gitHeadCommitDate.value.getOrElse("null")}"
+ |}""".stripMargin
+ )
+ Seq(file)
+ }
+ )
+ .dependsOn(
+ commonInfrastructure,
+ discussions.jvm(Dependencies.scalaVersion),
+ discussionsApi.jvm(Dependencies.scalaVersion),
+ discussionsImpl % s"$Test->$Test",
+ users.jvm(Dependencies.scalaVersion),
+ usersApi.jvm(Dependencies.scalaVersion),
+ usersImpl % s"$Test->$Test"
)
- .dependsOn(commonInfrastructure, discussionsJVM, usersJVM, discussionsApiJVM, usersApiJVM)
- .dependsOn(discussionsImpl % "it->it", usersImpl % "it->it")
val application = project
- .from("app")
- .setName("app")
- .setDescription("Branchtalk backend application")
- .configureModule
- .configureRun("io.branchtalk.Main")
+ .in(file("modules/app"))
+ .enablePlugins(GitVersioning, GitBranchPrompt)
+ .settings(
+ name := "app",
+ description := "Branchtalk backend application"
+ )
+ .settings(settings)
.settings(
+ Compile / run / mainClass := Some("io.branchtalk.Main"),
+ Compile / run / fork := true,
+ Compile / runMain / fork := true,
dockerUpdateLatest := true,
Docker / packageName := "branchtalk-server",
Docker / dockerExposedPorts := Seq(8080),
libraryDependencies ++= Seq(
Dependencies.logbackJackson,
- Dependencies.logbackJsonClassic,
+ Dependencies.logbackJsonClassic
),
- customPredef("scala.util.chaining", "cats.implicits", "eu.timepit.refined.auto")
+ customPredef("scala.util.chaining", "cats.implicits", "neotype")
+ )
+ .settings(
+ inTask(assembly)(
+ Seq(
+ assemblyJarName := s"${name.value}.jar",
+ assemblyMergeStrategy := {
+ // required for OpenAPIServer to work
+ case PathList("META-INF", "maven", "org.webjars", "swagger-ui", "pom.properties") =>
+ MergeStrategy.singleOrError
+ // conflicts on random crap
+ case "module-info.class" => MergeStrategy.discard
+ // otherwise
+ case strategy => MergeStrategy.defaultMergeStrategy(strategy)
+ },
+ mainClass := Some("io.branchtalk.Main")
+ )
+ )
)
.dependsOn(server, discussionsImpl, usersImpl)
+
+// aliases
+
+addCommandAlias("fmt", "scalafmt ; Test/scalafmt")
+addCommandAlias("fullTest", "test")
+addCommandAlias("fullCoverageTest", "coverage ; test ; coverageReport ; coverageAggregate")
diff --git a/modules/app/src/main/scala/cats/effect/IOLocalHack.scala b/modules/app/src/main/scala/cats/effect/IOLocalHack.scala
index 627895cf..efc466ec 100644
--- a/modules/app/src/main/scala/cats/effect/IOLocalHack.scala
+++ b/modules/app/src/main/scala/cats/effect/IOLocalHack.scala
@@ -3,5 +3,5 @@ package cats.effect
/** Hack allowing us to access the whole content of cats.effect.IOLocalState */
object IOLocalHack {
- def get: IO[scala.collection.immutable.Map[IOLocal[_], Any]] = IO.Local(state => (state, state))
+ def get: IO[scala.collection.immutable.Map[IOLocal[?], Any]] = IO.Local(state => (state, state))
}
diff --git a/modules/app/src/main/scala/io/branchtalk/Program.scala b/modules/app/src/main/scala/io/branchtalk/Program.scala
index 614329ba..c7c2feb1 100644
--- a/modules/app/src/main/scala/io/branchtalk/Program.scala
+++ b/modules/app/src/main/scala/io/branchtalk/Program.scala
@@ -2,34 +2,34 @@ package io.branchtalk
import cats.{ Functor, Monad }
import cats.effect.{ Async, ExitCode, Resource, Sync }
-import cats.effect.implicits._
+import cats.effect.implicits.*
import cats.effect.std.Dispatcher
import com.typesafe.config.ConfigFactory
import io.branchtalk.api.AppServer
import io.branchtalk.configs.{ APIConfig, AppArguments, Configuration }
import io.branchtalk.discussions.events.DiscussionEvent
import io.branchtalk.discussions.{ DiscussionsModule, DiscussionsReads, DiscussionsWrites }
-import io.branchtalk.logging.MDC
-import io.branchtalk.shared.infrastructure.{ ConsumerStream, DomainConfig, StreamRunner }
-import io.branchtalk.shared.model.{ Logger, UUIDGenerator }
+import io.branchtalk.logging.*
+import io.branchtalk.shared.infrastructure.*
+import io.branchtalk.shared.model.UUID
import io.branchtalk.users.{ UsersModule, UsersReads, UsersWrites }
import io.prometheus.client.CollectorRegistry
import org.http4s.metrics.prometheus.Prometheus
-import sun.misc.Signal // scalastyle:ignore illegal.imports
+import sun.misc.Signal
object Program {
- implicit protected val uuidGenerator: UUIDGenerator = UUIDGenerator.FastUUIDGenerator
+ protected given UUID.Generator = UUID.FastGenerator
def runApplication[F[_]: Async: MDC](args: List[String]): F[ExitCode] =
(for {
- implicit0(logger: Logger[F]) <- Logger.create[F]
+ given Logger[F] <- Logger.create[F]
env <- Configuration.getEnv[F]
appArguments <- AppArguments.parse[F](args, env)
- _ <- logger.info(show"Arguments passed: $appArguments")
+ _ <- Logger[F].info(show"Arguments passed: $appArguments")
_ <-
if (appArguments.isAnythingRun) initializeAndRunModules[F](appArguments)
- else logger.warn("Nothing to run, see --help for information how to turn on API server and projections")
+ else Logger[F].warn("Nothing to run, see --help for information how to turn on API server and projections")
} yield ExitCode.Success).handleError {
case noConfig @ AppArguments.NoConfig(help) =>
if (help.errors.nonEmpty) noConfig.printError()
@@ -39,23 +39,22 @@ object Program {
ExitCode.Error
}
- def resolveConfigs[F[_]: Sync](implicit logger: Logger[F]): F[(APIConfig, DomainConfig, DomainConfig)] = for {
- apiConfig <- Configuration.readConfig[F, APIConfig]("api")
- _ <- logger.info(show"App configs resolved to: ${apiConfig}")
- usersConfig <- Configuration.readConfig[F, DomainConfig]("users")
- _ <- logger.info(show"Users configs resolved to: ${usersConfig}")
- discussionsConfig <- Configuration.readConfig[F, DomainConfig]("discussions")
- _ <- logger.info(show"Discussions configs resolved to: ${discussionsConfig}")
- } yield (apiConfig, usersConfig, discussionsConfig)
+ def resolveConfigs[F[_]: Sync: Logger]: F[(APIConfig, DomainModule.Config, DomainModule.Config)] =
+ for {
+ apiConfig <- Configuration.readConfig[F, APIConfig]("api")
+ _ <- Logger[F].info(show"App configs resolved to: ${apiConfig}")
+ usersConfig <- Configuration.readConfig[F, DomainModule.Config]("users")
+ _ <- Logger[F].info(show"Users configs resolved to: ${usersConfig}")
+ discussionsConfig <- Configuration.readConfig[F, DomainModule.Config]("discussions")
+ _ <- Logger[F].info(show"Discussions configs resolved to: ${discussionsConfig}")
+ } yield (apiConfig, usersConfig, discussionsConfig)
- def initializeAndRunModules[F[_]: Async: MDC](
- appArguments: AppArguments
- )(implicit logger: Logger[F]): F[Unit] = {
+ def initializeAndRunModules[F[_]: Async: MDC: Logger](appArguments: AppArguments): F[Unit] = {
for {
- implicit0(dispacher: Dispatcher[F]) <- Dispatcher[F]
+ given Dispatcher[F] <- Dispatcher.parallel[F]
(apiConfig, usersConfig, discussionsConfig) <- Resource.eval(resolveConfigs[F])
registry <- Prometheus.collectorRegistry[F]
- modules <- Resource.make(logger.info("Initializing services"))(_ => logger.info("Services shut down")) >>
+ modules <- Resource.make(Logger[F].info("Initializing services"))(_ => Logger[F].info("Services shut down")) >>
(
registry.pure[Resource[F, *]],
UsersModule.reads[F](usersConfig, registry),
@@ -70,8 +69,7 @@ object Program {
run.tupled(modules)
}
- // scalastyle:off method.length parameter.number
- def runModules[F[_]: Async: MDC](
+ def runModules[F[_]: Async: MDC: Logger](
appArguments: AppArguments,
apiConfig: APIConfig,
terminationSignal: F[Unit],
@@ -85,7 +83,7 @@ object Program {
usersWrites: UsersWrites[F],
discussionsReads: DiscussionsReads[F],
discussionsWrites: DiscussionsWrites[F]
- )(implicit logger: Logger[F]): F[Unit] = {
+ ): F[Unit] = {
(
AppServer
.asResource(
@@ -121,7 +119,6 @@ object Program {
)
).tupled >> logBeforeAfter[F]("Services initialized", "Received exit signal")
}.use(_ => terminationSignal) // here we are blocking until e.g. user press Ctrl+C
- // scalastyle:on parameter.number method.length
// kudos to Łukasz Byczyński
private def awaitTerminationSignal[F[_]: Async]: F[Unit] = {
@@ -132,16 +129,16 @@ object Program {
handleSignal("INT").race(handleSignal("TERM")).void
}
- private def logBeforeAfter[F[_]: Functor](before: String, after: String)(implicit logger: Logger[F]) =
- Resource.make(logger.info(before))(_ => logger.info(after))
+ private def logBeforeAfter[F[_]: Functor](before: String, after: String)(using logger: Logger[F]) =
+ Resource.make(Logger[F].info(before))(_ => logger.info(after))
- implicit class ResourceOps[F[_]](private val resource: Resource[F, Unit]) extends AnyVal {
+ extension [F[_]: Monad: Logger](resource: Resource[F, Unit]) {
- def conditionally(name: String, condition: Boolean)(implicit F: Monad[F], logger: Logger[F]): Resource[F, Unit] =
+ def conditionally(name: String, condition: Boolean): Resource[F, Unit] =
if (condition) {
logBeforeAfter[F](s"Starting $name", s"$name shutdown completed") >>
resource >>
logBeforeAfter[F](s"$name start completed", s"Shutting down $name")
- } else Resource.eval(logger.info(s"$name disabled - omitting"))
+ } else Resource.eval(Logger[F].info(s"$name disabled - omitting"))
}
}
diff --git a/modules/app/src/main/scala/io/branchtalk/logging/IOGlobal.scala b/modules/app/src/main/scala/io/branchtalk/logging/IOGlobal.scala
index 810e48f5..155a5cbd 100644
--- a/modules/app/src/main/scala/io/branchtalk/logging/IOGlobal.scala
+++ b/modules/app/src/main/scala/io/branchtalk/logging/IOGlobal.scala
@@ -7,16 +7,16 @@ import scala.concurrent.ExecutionContext
import scala.concurrent.duration.FiniteDuration
/** Hack allowing us to:
- * - read whole content from `IO.Local` on every call that process user provided function through IOLocalHack
- * - put that content into `IOGlobal.threadLocal` so that unsafe functions integrating with no-FP code could
- * read it through `IOGlobal.getCurrent(ioLocal)`
+ * - read whole content from `IO.Local` on every call that process user provided function through IOLocalHack
+ * - put that content into `IOGlobal.threadLocal` so that unsafe functions integrating with no-FP code could read it
+ * through `IOGlobal.getCurrent(ioLocal)`
*
- * Requires passing `IOGlobal.configuredStatePropagation` instead of `IO.asyncForIO` into tagless final code.
+ * Requires passing `IOGlobal.configuredStatePropagation` instead of `IO.asyncForIO` into tagless final code.
*/
object IOGlobal {
- private val threadLocal: ThreadLocal[scala.collection.immutable.Map[IOLocal[_], Any]] =
- ThreadLocal.withInitial(() => scala.collection.immutable.Map.empty[IOLocal[_], Any])
+ private val threadLocal: ThreadLocal[scala.collection.immutable.Map[IOLocal[?], Any]] =
+ ThreadLocal.withInitial(() => scala.collection.immutable.Map.empty[IOLocal[?], Any])
private def propagateState[A](thunk: => IO[A]): IO[A] =
IOLocalHack.get.flatMap { state => threadLocal.set(state); thunk }
diff --git a/modules/app/src/main/scala/io/branchtalk/logging/IOMDCAdapter.scala b/modules/app/src/main/scala/io/branchtalk/logging/IOMDCAdapter.scala
index 297a17f0..d5d5f9ee 100644
--- a/modules/app/src/main/scala/io/branchtalk/logging/IOMDCAdapter.scala
+++ b/modules/app/src/main/scala/io/branchtalk/logging/IOMDCAdapter.scala
@@ -2,30 +2,33 @@ package io.branchtalk.logging
import cats.effect.{ IO, IOLocal }
-import java.{ util => ju }
-import ch.qos.logback.classic.util.LogbackMDCAdapter
+import java.util as ju
+import org.slf4j.spi.MDCAdapter
-import scala.jdk.CollectionConverters._
+import scala.jdk.CollectionConverters.*
// Based on solution described by OlegPy in https://olegpy.com/better-logging-monix-1/
// Using experimental hack: https://gist.github.com/MateuszKubuszok/d506706ee3c9b4c2291d47279f619523
-final class IOMDCAdapter(local: IOLocal[MDC.Ctx]) extends LogbackMDCAdapter {
+final class IOMDCAdapter(local: IOLocal[MDC.Ctx]) extends MDCAdapter {
+ // TODO: IOLocal[Map[String, List]]
- private def getMDC: MDC.Ctx = IOGlobal.getCurrent(local).getOrElse(Map.empty[String, String])
- private def setMDC(mdc: MDC.Ctx): Unit = IOGlobal.setTemporarily(local, mdc)
- private def update(f: MDC.Ctx => MDC.Ctx): Unit = setMDC(f(getMDC))
+ private def getMDC: MDC.Ctx = IOGlobal.getCurrent(local).getOrElse(Map.empty[String, String])
+ private def setMDC(mdc: MDC.Ctx): Unit = IOGlobal.setTemporarily(local, mdc)
+ private def update(f: MDC.Ctx => MDC.Ctx): Unit = setMDC(f(getMDC))
+ override def put(key: String, `val`: String): Unit = update(_.updated(key, `val`))
@SuppressWarnings(Array("org.wartremover.warts.Null")) // talking to Java interface
override def get(key: String): String = getMDC.get(key).orNull
- override def put(key: String, `val`: String): Unit = update(_.updated(key, `val`))
- override def remove(key: String): Unit = update(_.removed(key))
+ override def remove(key: String): Unit = update(_.removed(key))
+ override def clear(): Unit = setMDC(Map.empty)
- override def clear(): Unit = setMDC(Map.empty)
override def getCopyOfContextMap: ju.Map[String, String] = getMDC.asJava
- override def setContextMap(contextMap: ju.Map[String, String]): Unit = setMDC(contextMap.asScala.toMap)
+ override def setContextMap(contextMap: ju.Map[String, String] @unchecked): Unit = setMDC(contextMap.asScala.toMap)
- override def getPropertyMap: ju.Map[String, String] = getMDC.asJava
- override def getKeys: ju.Set[String] = getMDC.asJava.keySet()
+ override def pushByKey(key: String, value: String): Unit = ???
+ override def popByKey(key: String): String = ???
+ override def getCopyOfDequeByKey(key: String): ju.Deque[String] = ???
+ override def clearDequeByKey(key: String): Unit = ???
}
object IOMDCAdapter {
@@ -38,7 +41,7 @@ object IOMDCAdapter {
classOf[org.slf4j.MDC]
.getDeclaredField("mdcAdapter")
.tap(_.setAccessible(true))
- .set(null, new IOMDCAdapter(local)) // scalastyle:ignore null
+ .set(null, new IOMDCAdapter(local))
}
} yield new IOMDC(local)
}
diff --git a/modules/common-api-macros/src/main/resources/derive.semi.conf b/modules/common-api-macros/src/main/resources/derive.semi.conf
deleted file mode 100644
index aaef4d08..00000000
--- a/modules/common-api-macros/src/main/resources/derive.semi.conf
+++ /dev/null
@@ -1,4 +0,0 @@
-# wire Jsoniter Scala type classes for Catnip @Semi
-com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec=com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker.make
-# wire Tapir type classes for Catnip @Semi
-sttp.tapir.Schema=sttp.tapir.Schema.derived
diff --git a/modules/common-api-macros/src/main/resources/derive.stub.conf b/modules/common-api-macros/src/main/resources/derive.stub.conf
deleted file mode 100644
index a0973cb9..00000000
--- a/modules/common-api-macros/src/main/resources/derive.stub.conf
+++ /dev/null
@@ -1,3 +0,0 @@
-# allows JsCodec object as param of Catnip @Semi for Jsoniter Scala
-io.branchtalk.api.JsoniterSupport.JsCodec=com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec
-
diff --git a/modules/common-api/src/main/scala/io/branchtalk/api/Authentication.scala b/modules/common-api/src/main/scala/io/branchtalk/api/Authentication.scala
index 4c8dd593..4022a09e 100644
--- a/modules/common-api/src/main/scala/io/branchtalk/api/Authentication.scala
+++ b/modules/common-api/src/main/scala/io/branchtalk/api/Authentication.scala
@@ -1,16 +1,11 @@
package io.branchtalk.api
-import io.branchtalk.ADT
-
-sealed trait Authentication extends ADT {
+enum Authentication {
+ case Session(sessionID: SessionID)
+ case Credentials(username: Username, password: Password)
def fold[B](session: SessionID => B, credentials: (Username, Password) => B): B = this match {
case Authentication.Session(sessionID) => session(sessionID)
case Authentication.Credentials(username, password) => credentials(username, password)
}
}
-object Authentication {
-
- final case class Session(sessionID: SessionID) extends Authentication
- final case class Credentials(username: Username, password: Password) extends Authentication
-}
diff --git a/modules/common-api/src/main/scala/io/branchtalk/api/AuthenticationSupport.scala b/modules/common-api/src/main/scala/io/branchtalk/api/AuthenticationSupport.scala
index b460713d..0bb33893 100644
--- a/modules/common-api/src/main/scala/io/branchtalk/api/AuthenticationSupport.scala
+++ b/modules/common-api/src/main/scala/io/branchtalk/api/AuthenticationSupport.scala
@@ -1,54 +1,44 @@
package io.branchtalk.api
import java.util.Base64
-import cats.effect.SyncIO
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.collection.NonEmpty
+import cats.effect.{ GenSpawn, SyncIO }
import io.branchtalk.api.Authentication.{ Credentials, Session }
-import io.branchtalk.shared.model.{ ParseRefined, UUIDGenerator, branchtalkCharset }
-import io.branchtalk.shared.model.UUIDGenerator.FastUUIDGenerator
-import sttp.tapir._
+import io.branchtalk.shared.model.*
+import sttp.tapir.*
import scala.util.Try
// Authentication-related definitions for Tapir.
object AuthenticationSupport {
- private object base64 { // scalastyle:ignore object.name
- def apply(string: String): String = Base64.getEncoder.encodeToString(string.getBytes(branchtalkCharset))
+ private object base64 {
+ def apply(string: String): String = Base64.getEncoder.encodeToString(string.getBytes(branchtalkCharset))
def unapply(string: String): Option[String] =
Try(new String(Base64.getDecoder.decode(string), branchtalkCharset)).toOption
}
- private val basicR = raw"Basic (.+)".r
- private val upR = raw"([^:]+):(.+)".r
- private object basic { // scalastyle:ignore object.name
- def apply(username: String, password: Array[Byte] Refined NonEmpty): String =
- s"""Basic ${base64(s"${username}:${new String(password.value, branchtalkCharset)}")}"""
- def unapply(string: String): Option[(String, Array[Byte] Refined NonEmpty)] = string match {
- case basicR(base64(upR(username, password))) =>
- ParseRefined[SyncIO]
- .parse[NonEmpty](password.getBytes(branchtalkCharset))
- .attempt
- .unsafeRunSync()
- .toOption
- .map(username -> _)
- case _ => None
+ private object basic {
+ private val basicR = raw"Basic (.+)".r
+ private val upR = raw"([^:]+):(.+)".r
+ def apply(username: String, password: Array[Byte]): String =
+ s"""Basic ${base64(s"${username}:${new String(password, branchtalkCharset)}")}"""
+ def unapply(string: String): Option[(String, Array[Byte])] = string match {
+ case basicR(base64(upR(username, password))) => Some(username -> password.getBytes(branchtalkCharset))
+ case _ => None
}
}
- private val bearerR = raw"Bearer (.+)".r
- private object bearer { // scalastyle:ignore object.name
+ private object bearer {
+ private val bearerR = raw"Bearer (.+)".r
def apply(sessionID: String): String = s"""Bearer ${sessionID}"""
- def unapply(string: String): Option[String] = string match {
+ def unapply(string: String): Option[String] = string match {
case bearerR(sessionID) => Some(sessionID.trim)
case _ => None
}
}
- implicit private class ResultOps[A](private val io: SyncIO[A]) extends AnyVal {
-
- def asResult(original: String): DecodeResult[A] = io.attempt.unsafeRunSync() match {
+ extension [A](io: SyncIO[A]) {
+ private def asResult(original: String): DecodeResult[A] = io.attempt.unsafeRunSync() match {
case Left(value) => DecodeResult.Error(original, value)
case Right(value) => DecodeResult.Value(value)
}
@@ -60,15 +50,15 @@ object AuthenticationSupport {
case original if original.startsWith("Basic") =>
DecodeResult.Error(original, new Exception("Expected base64-encoded username:password"))
case original @ bearer(sessionID) =>
- implicit val uuidGenerator: UUIDGenerator.FastUUIDGenerator.type = FastUUIDGenerator // passing it down it PITA
+ given UUID.Generator = UUID.FastGenerator // passing it is a PITA
SessionID.parse[SyncIO](sessionID).map(Session.apply).asResult(original)
case original if original.startsWith("Bearer") =>
DecodeResult.Error(original, new Exception("Expected session ID"))
case original =>
DecodeResult.Error(original, new Exception("Unknown authentication type"))
} {
- case Session(sessionID) => bearer(sessionID.uuid.show)
- case Credentials(username, password) => basic(username.nonEmptyString.value, password.nonEmptyBytes)
+ case Session(sessionID) => bearer(sessionID.show)
+ case Credentials(username, password) => basic(username.unwrap, password.unwrap)
}
val authHeader: EndpointIO.Header[Authentication] = header[String]("Authentication")
.map(authHeaderMapping)
diff --git a/modules/common-api/src/main/scala/io/branchtalk/api/JsoniterSupport.scala b/modules/common-api/src/main/scala/io/branchtalk/api/JsoniterSupport.scala
index 9c37831b..87953e4f 100644
--- a/modules/common-api/src/main/scala/io/branchtalk/api/JsoniterSupport.scala
+++ b/modules/common-api/src/main/scala/io/branchtalk/api/JsoniterSupport.scala
@@ -2,106 +2,110 @@ package io.branchtalk.api
import cats.{ Id, Order }
import cats.data.{ Chain, NonEmptyChain, NonEmptyList, NonEmptySet }
-import com.github.plokhotnyuk.jsoniter_scala.core._
-import com.github.plokhotnyuk.jsoniter_scala.macros._
-import eu.timepit.refined.api.{ Refined, Validate }
-import eu.timepit.refined.refineV
-import io.branchtalk.shared.model.{ ID, OptionUpdatable, UUID, Updatable, discriminatorNameMapper }
-import io.estatico.newtype.Coercible
-
-// Provides (missing :/) support for .map, .mapDecode, .refine, .asNewtype for Jsoniter Scala codecs.
-// TODO: consider moving to some external library
-@SuppressWarnings(Array("org.wartremover.warts.All")) // handling valid null values, macros
-object JsoniterSupport {
-
- // shortcut with object allowing consistent usage of @Semi(JsCodec)
- type JsCodec[A] = JsonValueCodec[A]
- object JsCodec
+import com.github.plokhotnyuk.jsoniter_scala.core.*
+import com.github.plokhotnyuk.jsoniter_scala.macros.*
+import io.branchtalk.shared.model.*
+
+// Provides (missing :/) support for .map, .mapDecode,.asNewtype for Jsoniter Scala codecs.
+object JsoniterSupport extends JsoniterSupportImplicits {
+
+ // for shortening
- @inline def summonCodec[T](implicit codec: JsCodec[T]): JsCodec[T] = codec
+ type JsCodec[A] = JsonValueCodec[A]
+ export com.github.plokhotnyuk.jsoniter_scala.macros.{
+ CodecMakerConfig as JsCodecConfig,
+ ConfiguredJsonValueCodec as DefaultJsCodec
+ }
// utilities
- implicit class RefineCodec[T](private val codec: JsCodec[T]) extends AnyVal {
+ inline def summonCodec[T](using codec: JsCodec[T]): JsCodec[T] = codec
- def mapDecode[U](f: T => Either[String, U])(g: U => T): JsCodec[U] = new JsCodec[U] {
- override def decodeValue(in: JsonReader, default: U): U =
- codec.decodeValue(in, if (default != null) g(default) else null.asInstanceOf[T]) match { // scalastyle:ignore
- case null => null.asInstanceOf[U] // scalastyle:ignore
+ extension [A](codec: JsCodec[A]) {
+ @SuppressWarnings(Array("org.wartremover.warts.All"))
+ def mapDecode[B](f: A => Either[String, B])(g: B => A): JsCodec[B] = new JsCodec[B] {
+ override def decodeValue(in: JsonReader, default: B): B =
+ codec.decodeValue(in, if (default != null) g(default) else null.asInstanceOf[A]) match {
+ case null => null.asInstanceOf[B]
case t =>
f(t) match {
- case null => null.asInstanceOf[U] // scalastyle:ignore
+ case null => null.asInstanceOf[B]
case Left(error) => in.decodeError(error)
case Right(value) => value
}
}
- override def encodeValue(x: U, out: JsonWriter): Unit = codec.encodeValue(g(x), out)
+ override def encodeValue(x: B, out: JsonWriter): Unit = codec.encodeValue(g(x), out)
- override def nullValue: U = codec.nullValue match {
- case null => null.asInstanceOf[U] // scalastyle:ignore
- case u => f(u).getOrElse(null.asInstanceOf[U]) // scalastyle:ignore
+ override def nullValue: B = codec.nullValue match {
+ case null => null.asInstanceOf[B]
+ case u => f(u).getOrElse(null.asInstanceOf[B])
}
}
- def map[U](f: T => U)(g: U => T): JsCodec[U] = new JsCodec[U] {
- override def decodeValue(in: JsonReader, default: U): U =
- codec.decodeValue(in, if (default != null) g(default) else null.asInstanceOf[T]) match { // scalastyle:ignore
- case null => null.asInstanceOf[U] // scalastyle:ignore
+ @SuppressWarnings(Array("org.wartremover.warts.All"))
+ def map[B](f: A => B)(g: B => A): JsCodec[B] = new JsCodec[B] {
+ override def decodeValue(in: JsonReader, default: B): B =
+ codec.decodeValue(in, if (default != null) g(default) else null.asInstanceOf[A]) match {
+ case null => null.asInstanceOf[B]
case t => f(t)
}
- override def encodeValue(x: U, out: JsonWriter): Unit = codec.encodeValue(g(x), out)
+ override def encodeValue(x: B, out: JsonWriter): Unit = codec.encodeValue(g(x), out)
- override def nullValue: U = codec.nullValue match {
- case null => null.asInstanceOf[U] // scalastyle:ignore
+ override def nullValue: B = codec.nullValue match {
+ case null => null.asInstanceOf[B]
case u => f(u)
}
}
- def refine[P: Validate[T, *]]: JsCodec[T Refined P] = mapDecode(refineV[P](_: T))(_.value)
-
- def asNewtype[N: Coercible[T, *]]: JsCodec[N] = Coercible.unsafeWrapMM[JsCodec, Id, T, N].apply(codec)
+ def asNewtypeCodec[B](using newtype: Newtype.WithType[A, B]): JsCodec[B] =
+ newtype.unsafeMakeF[JsCodec](codec)
}
// domain instances
- implicit def idCodec[A]: JsCodec[ID[A]] = summonCodec[UUID](JsonCodecMaker.make).asNewtype[ID[A]]
+ given [A]: JsCodec[ID[A]] = DefaultJsCodec.derived[UUID].asNewtypeCodec[ID[A]]
- implicit def updatableCodec[A: JsCodec]: JsCodec[Updatable[A]] = summonCodec[Updatable[A]](
- JsonCodecMaker.make(
- CodecMakerConfig
- .withAdtLeafClassNameMapper(discriminatorNameMapper("."))
- .withDiscriminatorFieldName(Some("action"))
- )
- )
+ given [A](using JsCodec[A]): JsCodec[Updatable[A]] = {
+ inline given CodecMakerConfig =
+ CodecMakerConfig.withAdtLeafClassNameMapper(adtDiscriminatorNameMapper).withDiscriminatorFieldName(Some("action"))
+ DefaultJsCodec.derived[Updatable[A]]
+ }
- implicit def optionUpdatableCodec[A: JsCodec]: JsCodec[OptionUpdatable[A]] = summonCodec[OptionUpdatable[A]](
- JsonCodecMaker.make(
- CodecMakerConfig
- .withAdtLeafClassNameMapper(discriminatorNameMapper("."))
- .withDiscriminatorFieldName(Some("action"))
- )
- )
+ given [A](using JsCodec[A]): JsCodec[OptionUpdatable[A]] = {
+ inline given CodecMakerConfig =
+ CodecMakerConfig.withAdtLeafClassNameMapper(adtDiscriminatorNameMapper).withDiscriminatorFieldName(Some("action"))
+ DefaultJsCodec.derived[OptionUpdatable[A]]
+ }
// Cats instances
- implicit def chainCodec[A: JsCodec]: JsCodec[Chain[A]] =
- summonCodec[List[A]](JsonCodecMaker.make).map(Chain.fromSeq)(_.toList)
-
- implicit def necCodec[A: JsCodec]: JsCodec[NonEmptyChain[A]] = summonCodec[List[A]](JsonCodecMaker.make).mapDecode {
- case head :: tail => NonEmptyChain(head, tail: _*).asRight[String]
- case _ => "Expected non-empty list".asLeft[NonEmptyChain[A]]
- }(_.toList)
-
- implicit def nelCodec[A: JsCodec]: JsCodec[NonEmptyList[A]] = summonCodec[List[A]](JsonCodecMaker.make).mapDecode {
- case head :: tail => NonEmptyList(head, tail).asRight[String]
- case _ => "Expected non-empty list".asLeft[NonEmptyList[A]]
- }(_.toList)
-
- implicit def nesCodec[A: JsCodec: Order]: JsCodec[NonEmptySet[A]] =
- summonCodec[List[A]](JsonCodecMaker.make).mapDecode {
+ given [A: JsCodec]: JsCodec[Chain[A]] = DefaultJsCodec.derived[List[A]].map(Chain.fromSeq)(_.toList)
+ given [A: JsCodec]: JsCodec[NonEmptyChain[A]] = DefaultJsCodec
+ .derived[List[A]]
+ .mapDecode {
+ case head :: tail => NonEmptyChain(head, tail: _*).asRight[String]
+ case _ => "Expected non-empty list".asLeft[NonEmptyChain[A]]
+ }(_.toList)
+ given [A: JsCodec]: JsCodec[NonEmptyList[A]] = DefaultJsCodec
+ .derived[List[A]]
+ .mapDecode {
+ case head :: tail => NonEmptyList(head, tail).asRight[String]
+ case _ => "Expected non-empty list".asLeft[NonEmptyList[A]]
+ }(_.toList)
+ given [A: JsCodec: Order]: JsCodec[NonEmptySet[A]] = DefaultJsCodec
+ .derived[List[A]]
+ .mapDecode {
case head :: tail => NonEmptySet.of(head, tail: _*).asRight[String]
case _ => "Expected non-empty list".asLeft[NonEmptySet[A]]
}(_.toList)
+
+ // Neotype
+
+ export neotype.interop.jsoniter.{ newtypeCodec, subtypeCodec }
+}
+private[api] trait JsoniterSupportImplicits { self: JsoniterSupport.type =>
+
+ inline given CodecMakerConfig = CodecMakerConfig
}
diff --git a/modules/common-api/src/main/scala/io/branchtalk/api/Pagination.scala b/modules/common-api/src/main/scala/io/branchtalk/api/Pagination.scala
index 11ddb378..bca63712 100644
--- a/modules/common-api/src/main/scala/io/branchtalk/api/Pagination.scala
+++ b/modules/common-api/src/main/scala/io/branchtalk/api/Pagination.scala
@@ -1,30 +1,65 @@
package io.branchtalk.api
-import io.branchtalk.api.JsoniterSupport._
-import io.branchtalk.api.TapirSupport.JsSchema
-import io.scalaland.chimney.dsl._
-import io.branchtalk.shared.model.Paginated
-import io.scalaland.catnip.Semi
+import cats.effect.Sync
+import io.branchtalk.api.JsoniterSupport.*
+import io.branchtalk.api.TapirSupport.*
+import io.scalaland.chimney.dsl.*
+import io.branchtalk.shared.model.*
-@Semi(JsCodec, JsSchema) final case class Pagination[A](
+final case class Pagination[A](
entities: List[A],
- offset: PaginationOffset,
- limit: PaginationLimit,
- nextOffset: Option[PaginationOffset]
-)
+ offset: Pagination.Offset,
+ limit: Pagination.Limit,
+ nextOffset: Option[Pagination.Offset]
+) derives DefaultJsCodec,
+ JsSchema
-@SuppressWarnings(Array("org.wartremover.warts.All")) // for macros
object Pagination {
def fromPaginated[Entity](
paginated: Paginated[Entity],
- offset: PaginationOffset,
- limit: PaginationLimit
+ offset: Paginated.Offset,
+ limit: Paginated.Limit
): Pagination[Entity] =
paginated
.into[Pagination[Entity]]
- .withFieldConst(_.offset, offset)
- .withFieldConst(_.limit, limit)
- .withFieldComputed(_.nextOffset, p => p.nextOffset.map(PaginationOffset(_)))
+ .withFieldConst(_.offset, Offset.unsafeMake(offset.unwrap))
+ .withFieldConst(_.limit, Limit.unsafeMake(limit.unwrap))
+ .withFieldComputed(_.nextOffset, _.nextOffset.map(o => Offset.unsafeMake(o.unwrap)))
.transform
+
+ type Offset = Offset.Type
+ object Offset extends Newtype[Long] {
+
+ override inline def validate(input: Long): Boolean = input >= 0L
+
+ def unapply(offset: Offset): Some[Long] = Some(offset.unwrap)
+ def parse[F[_]: Sync](long: Long): F[Offset] = ParseNewtype[F].parse[Offset](long)
+
+ given JsCodec[Offset] = DefaultJsCodec.derived[Long].asNewtypeCodec[Offset]
+ given Param[Offset] = summonParam[Long].mapDecode(l => DecodeResult.fromEitherString(l.toString, make(l)))(_.unwrap)
+ given JsSchema[Offset] = summonSchema[Long].asNewtypeSchema[Offset]
+ }
+
+ type Limit = Limit.Type
+ object Limit extends Newtype[Int] {
+
+ override inline def validate(input: Int): Boolean = input > 0
+
+ def unapply(limit: Limit): Some[Int] = Some(limit.unwrap)
+ def parse[F[_]: Sync](long: Int): F[Limit] = ParseNewtype[F].parse[Limit](long)
+
+ given JsCodec[Limit] = DefaultJsCodec.derived[Int].asNewtypeCodec[Limit]
+ given Param[Limit] = summonParam[Int].mapDecode(l => DecodeResult.fromEitherString(l.toString, make(l)))(_.unwrap)
+ given JsSchema[Limit] = summonSchema[Int].asNewtypeSchema[Limit]
+ }
+
+ type HasNext = HasNext.Type
+ object HasNext extends Newtype[Boolean] {
+
+ def unapply(hasNext: HasNext): Some[Boolean] = Some(hasNext.unwrap)
+
+ given JsCodec[HasNext] = DefaultJsCodec.derived[Boolean].asNewtypeCodec[HasNext]
+ given JsSchema[HasNext] = summonSchema[Boolean].asNewtypeSchema[HasNext]
+ }
}
diff --git a/modules/common-api/src/main/scala/io/branchtalk/api/Permission.scala b/modules/common-api/src/main/scala/io/branchtalk/api/Permission.scala
index e574bed7..8111505a 100644
--- a/modules/common-api/src/main/scala/io/branchtalk/api/Permission.scala
+++ b/modules/common-api/src/main/scala/io/branchtalk/api/Permission.scala
@@ -1,31 +1,28 @@
package io.branchtalk.api
import cats.Order
-import io.branchtalk.ADT
-import io.branchtalk.api.JsoniterSupport._
-import io.branchtalk.shared.model.{ ShowPretty, UUID }
-import io.scalaland.catnip.Semi
+import io.branchtalk.api.JsoniterSupport.*
+import io.branchtalk.shared.model.*
-@Semi(ShowPretty, JsCodec) sealed trait Permission extends ADT
-@SuppressWarnings(Array("org.wartremover.warts.All")) // macros
+enum Permission derives ShowPretty, DefaultJsCodec {
+ case Administrate
+ case IsOwner
+ case ModerateUsers
+ case ModerateChannel(channelID: ChannelID)
+ case CanPublish(channelID: ChannelID)
+}
object Permission {
- case object Administrate extends Permission
- case object IsOwner extends Permission
- case object ModerateUsers extends Permission
- final case class ModerateChannel(channelID: ChannelID) extends Permission
- final case class CanPublish(channelID: ChannelID) extends Permission
-
- implicit val order: Order[Permission] = {
+ given Order[Permission] = {
case (Administrate, Administrate) => 0
case (Administrate, _) => 1
case (IsOwner, IsOwner) => 0
case (IsOwner, _) => 1
case (ModerateUsers, ModerateUsers) => 0
case (ModerateUsers, _) => 1
- case (ModerateChannel(c1), ModerateChannel(c2)) => Order[UUID].compare(c1.uuid, c2.uuid)
+ case (ModerateChannel(c1), ModerateChannel(c2)) => Order[UUID].compare(c1.unwrap, c2.unwrap)
case (ModerateChannel(_), _) => 1
- case (CanPublish(c1), CanPublish(c2)) => Order[UUID].compare(c1.uuid, c2.uuid)
+ case (CanPublish(c1), CanPublish(c2)) => Order[UUID].compare(c1.unwrap, c2.unwrap)
case (CanPublish(_), _) => -1
}
}
diff --git a/modules/common-api/src/main/scala/io/branchtalk/api/RequiredPermissions.scala b/modules/common-api/src/main/scala/io/branchtalk/api/RequiredPermissions.scala
index dc6fbccd..f293b318 100644
--- a/modules/common-api/src/main/scala/io/branchtalk/api/RequiredPermissions.scala
+++ b/modules/common-api/src/main/scala/io/branchtalk/api/RequiredPermissions.scala
@@ -2,57 +2,57 @@ package io.branchtalk.api
import cats.Eq
import cats.data.{ NonEmptyList, NonEmptySet }
-import com.github.plokhotnyuk.jsoniter_scala.macros._
-import io.branchtalk.ADT
-import io.branchtalk.api.JsoniterSupport._
-import io.branchtalk.shared.model.{ FastEq, ShowPretty }
-import io.scalaland.catnip.Semi
-
-@Semi(FastEq, ShowPretty) sealed trait RequiredPermissions extends ADT {
-
- // scalastyle:off method.name
- def &&(other: RequiredPermissions): RequiredPermissions = RequiredPermissions.And(this, other)
- def ||(other: RequiredPermissions): RequiredPermissions = RequiredPermissions.Or(this, other)
- def unary_! : RequiredPermissions = RequiredPermissions.Not(this)
- // scalastyle:on method.name
+import com.github.plokhotnyuk.jsoniter_scala.macros.*
+import io.branchtalk.api.JsoniterSupport.{ *, given }
+import io.branchtalk.shared.model.{ FastEq, ShowPretty, void }
+
+import scala.annotation.targetName
+
+enum RequiredPermissions derives FastEq, ShowPretty {
+ case Empty
+
+ case AllOf(toSet: NonEmptySet[Permission])
+ case AnyOf(toSet: NonEmptySet[Permission])
+
+ case And(x: RequiredPermissions, y: RequiredPermissions)
+ case Or(x: RequiredPermissions, y: RequiredPermissions)
+ case Not(x: RequiredPermissions)
+
+ @targetName("and") def &&(other: RequiredPermissions): RequiredPermissions = And(this, other)
+ @targetName("or") def ||(other: RequiredPermissions): RequiredPermissions = Or(this, other)
+ @targetName("not") def unary_! : RequiredPermissions = Not(this)
}
object RequiredPermissions {
- def empty: RequiredPermissions = Empty
- def one(permission: Permission): RequiredPermissions = AllOf(NonEmptySet.one(permission))
+ def empty: RequiredPermissions = Empty
+ def one(permission: Permission): RequiredPermissions = AllOf(NonEmptySet.one(permission))
def allOf(head: Permission, tail: Permission*): RequiredPermissions = AllOf(NonEmptySet.of(head, tail: _*))
def anyOf(head: Permission, tail: Permission*): RequiredPermissions = AnyOf(NonEmptySet.of(head, tail: _*))
- case object Empty extends RequiredPermissions
+ given JsCodec[RequiredPermissions] = {
+ transparent inline given CodecMakerConfig = CodecMakerConfig.withAllowRecursiveTypes(true)
+ DefaultJsCodec.derived[RequiredPermissions]
+ }
- final case class AllOf(toSet: NonEmptySet[Permission]) extends RequiredPermissions
- final case class AnyOf(toSet: NonEmptySet[Permission]) extends RequiredPermissions
-
- final case class And(x: RequiredPermissions, y: RequiredPermissions) extends RequiredPermissions
- final case class Or(x: RequiredPermissions, y: RequiredPermissions) extends RequiredPermissions
- final case class Not(x: RequiredPermissions) extends RequiredPermissions
-
- @SuppressWarnings(Array("org.wartremover.warts.All")) // handling valid null values
- implicit val jsCodec: JsCodec[RequiredPermissions] =
- summonCodec[RequiredPermissions](JsonCodecMaker.make(CodecMakerConfig.withAllowRecursiveTypes(true)))
-
- implicit val nesEq: Eq[NonEmptySet[Permission]] = (x: NonEmptySet[Permission], y: NonEmptySet[Permission]) =>
+ given Eq[NonEmptySet[Permission]] = (x: NonEmptySet[Permission], y: NonEmptySet[Permission]) =>
x.toSortedSet === y.toSortedSet
- implicit val nesShow: ShowPretty[NonEmptySet[Permission]] =
- (t: NonEmptySet[Permission], sb: StringBuilder, indentWith: String, indentLevel: Int) => {
+
+ @SuppressWarnings(Array("org.wartremover.warts.MutableDataStructures"))
+ given ShowPretty[NonEmptySet[Permission]] = {
+ (t: NonEmptySet[Permission], sb: StringBuilder, indentWith: String, indentLevel: Int) =>
val nextIndent = indentLevel + 1
- sb.append(indentWith * indentLevel).append("NonEmptySet(\n")
+ void(sb.append(indentWith * indentLevel).append("NonEmptySet(\n"))
t.toNonEmptyList match {
case NonEmptyList(head, tail) =>
sb.append(indentWith * nextIndent)
- implicitly[ShowPretty[Permission]].showPretty(head, sb, indentWith, nextIndent)
+ void(summon[ShowPretty[Permission]].showPretty(head, sb, indentWith, nextIndent))
tail.foreach { elem =>
sb.append(",\n")
sb.append(indentWith * nextIndent)
- implicitly[ShowPretty[Permission]].showPretty(elem, sb, indentWith, nextIndent)
+ summon[ShowPretty[Permission]].showPretty(elem, sb, indentWith, nextIndent)
}
sb.append("\n)")
}
sb
- }
+ }
}
diff --git a/modules/common-api/src/main/scala/io/branchtalk/api/ServerErrorHandler.scala b/modules/common-api/src/main/scala/io/branchtalk/api/ServerErrorHandler.scala
index 0e9bd56e..1118651a 100644
--- a/modules/common-api/src/main/scala/io/branchtalk/api/ServerErrorHandler.scala
+++ b/modules/common-api/src/main/scala/io/branchtalk/api/ServerErrorHandler.scala
@@ -10,8 +10,7 @@ trait ServerErrorHandler[F[_], E] {
}
object ServerErrorHandler {
- @inline def apply[F[_], E](implicit serverErrorHandler: ServerErrorHandler[F, E]): ServerErrorHandler[F, E] =
- serverErrorHandler
+ inline def apply[F[_], E](using handler: ServerErrorHandler[F, E]): ServerErrorHandler[F, E] = handler
def handleCommonErrors[F[_]: Sync, E](mapping: CommonError => E)(logger: Logger): ServerErrorHandler[F, E] =
new ServerErrorHandler[F, E] {
diff --git a/modules/common-api/src/main/scala/io/branchtalk/api/TapirSupport.scala b/modules/common-api/src/main/scala/io/branchtalk/api/TapirSupport.scala
index 9d214ff5..66e34e30 100644
--- a/modules/common-api/src/main/scala/io/branchtalk/api/TapirSupport.scala
+++ b/modules/common-api/src/main/scala/io/branchtalk/api/TapirSupport.scala
@@ -1,23 +1,16 @@
package io.branchtalk.api
import java.net.URI
-
import cats.Id
import cats.data.{ Chain, NonEmptyChain, NonEmptyList, NonEmptySet }
-import io.branchtalk.shared.model.{ ID, OptionUpdatable, UUID, Updatable, discriminatorNameMapper }
-import io.estatico.newtype.Coercible
+import io.branchtalk.shared.model.*
import sttp.tapir.CodecFormat.TextPlain
import sttp.tapir.generic.Configuration
import scala.annotation.nowarn
-// Allows `import TapirSupport._` instead of `import sttp.tapir._, sttp.tapir.codec.refined._, ...`.
-@nowarn("cat=unused")
-object TapirSupport
- extends sttp.tapir.Tapir
- with sttp.tapir.TapirAliases
- with sttp.tapir.codec.refined.TapirCodecRefined
- with sttp.tapir.json.jsoniter.TapirJsonJsoniter {
+// Allows `import TapirSupport._` instead of `import sttp.tapir._, sttp.tapir.json.jsoniter._, ...`.
+object TapirSupport extends sttp.tapir.Tapir, sttp.tapir.TapirAliases, sttp.tapir.json.jsoniter.TapirJsonJsoniter {
// shortcuts
type Param[A] = sttp.tapir.Codec[String, A, TextPlain]
@@ -26,67 +19,54 @@ object TapirSupport
type JsSchema[A] = sttp.tapir.Schema[A]
val JsSchema = sttp.tapir.Schema
- @inline def summonParam[T](implicit param: Param[T]): Param[T] = param
- @inline def summonSchema[T](implicit schema: JsSchema[T]): JsSchema[T] = schema
+ inline def summonParam[T](using param: Param[T]): Param[T] = param
+ inline def summonSchema[T](using schema: JsSchema[T]): JsSchema[T] = schema
// utilities
- implicit class EndpointOps[A, I, E, O, R](private val endpoint: Endpoint[A, I, E, O, R]) extends AnyVal {
-
+ extension [A, I, E, O, R](endpoint: Endpoint[A, I, E, O, R]) {
def notRequiringPermissions: AuthedEndpoint[A, I, E, O, R] =
AuthedEndpoint(endpoint, _ => RequiredPermissions.empty)
-
def requiringPermissions(permissions: I => RequiredPermissions): AuthedEndpoint[A, I, E, O, R] =
AuthedEndpoint(endpoint, permissions)
}
- implicit class TapirResultOps[A](private val decodeResult: DecodeResult[A]) extends AnyVal {
-
+ extension [A](decodeResult: DecodeResult[A]) {
def toOption: Option[A] = decodeResult match {
case DecodeResult.Value(v) => v.some
case _ => none[A]
}
}
- implicit class RefineSchema[T](private val schema: JsSchema[T]) extends AnyVal {
-
- def asNewtype[N: Coercible[T, *]]: JsSchema[N] = Coercible.unsafeWrapMM[JsSchema, Id, T, N].apply(schema)
+ extension [A](schema: JsSchema[A]) {
+ def asNewtypeSchema[B](using newtype: Newtype.WithType[A, B]): JsSchema[B] =
+ newtype.unsafeMakeF[JsSchema](schema)
}
- implicit val uriSchema: JsSchema[URI] = JsSchema.schemaForString.asInstanceOf[JsSchema[URI]]
+ given [A, B](using A: Newtype.WithType[B, A], B: JsSchema[B]): JsSchema[A] = B.asNewtypeSchema[A]
+
+ given JsSchema[URI] = JsSchema.schemaForString.as[URI]
// domain instances
- implicit def idParam[A]: Param[ID[A]] = summonParam[UUID].map[ID[A]](ID[A](_))(_.uuid)
- implicit def idSchema[A]: JsSchema[ID[A]] = summonSchema[UUID].asNewtype[ID[A]]
+ given [A]: Param[ID[A]] = summonParam[UUID].map[ID[A]](ID[A].apply)(_.unwrap)
+ given [A]: JsSchema[ID[A]] = summonSchema[UUID].asNewtypeSchema[ID[A]]
- implicit def updatableSchema[A: JsSchema]: JsSchema[Updatable[A]] = {
- implicit val customConfiguration: Configuration =
- Configuration.default.copy(toEncodedName = discriminatorNameMapper(".")).withDiscriminator("action")
+ given [A: JsSchema]: JsSchema[Updatable[A]] = {
+ given Configuration =
+ Configuration.default.copy(toEncodedName = adtDiscriminatorNameMapper).withDiscriminator("action")
JsSchema.derived[Updatable[A]]
}
-
- implicit def optionUpdatableSchema[A: JsSchema]: JsSchema[OptionUpdatable[A]] = {
- implicit val customConfiguration: Configuration =
- Configuration.default.copy(toEncodedName = discriminatorNameMapper(".")).withDiscriminator("action")
+ given [A: JsSchema]: JsSchema[OptionUpdatable[A]] = {
+ given Configuration =
+ Configuration.default.copy(toEncodedName = adtDiscriminatorNameMapper).withDiscriminator("action")
JsSchema.derived[OptionUpdatable[A]]
}
/// Cats codecs
- @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
- implicit def chainSchema[A: JsSchema]: JsSchema[Chain[A]] =
- summonSchema[List[A]].asInstanceOf[JsSchema[Chain[A]]] // scalastyle:ignore
-
- @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
- implicit def necSchema[A: JsSchema]: JsSchema[NonEmptyChain[A]] =
- summonSchema[List[A]].asInstanceOf[JsSchema[NonEmptyChain[A]]] // scalastyle:ignore
-
- @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
- implicit def nelSchema[A: JsSchema]: JsSchema[NonEmptyList[A]] =
- summonSchema[List[A]].asInstanceOf[JsSchema[NonEmptyList[A]]] // scalastyle:ignore
-
- @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
- implicit def nesSchema[A: JsSchema]: JsSchema[NonEmptySet[A]] =
- summonSchema[List[A]].asInstanceOf[JsSchema[NonEmptySet[A]]] // scalastyle:ignore
+ given [A: JsSchema]: JsSchema[Chain[A]] = summonSchema[List[A]].as[Chain[A]]
+ given [A: JsSchema]: JsSchema[NonEmptyChain[A]] = summonSchema[List[A]].as[NonEmptyChain[A]]
+ given [A: JsSchema]: JsSchema[NonEmptyList[A]] = summonSchema[List[A]].as[NonEmptyList[A]]
+ given [A: JsSchema]: JsSchema[NonEmptySet[A]] = summonSchema[List[A]].as[NonEmptySet[A]]
}
diff --git a/modules/common-api/src/main/scala/io/branchtalk/api/authedendpoints.scala b/modules/common-api/src/main/scala/io/branchtalk/api/authedendpoints.scala
index 6cde5bf4..6abb7eb6 100644
--- a/modules/common-api/src/main/scala/io/branchtalk/api/authedendpoints.scala
+++ b/modules/common-api/src/main/scala/io/branchtalk/api/authedendpoints.scala
@@ -2,7 +2,7 @@ package io.branchtalk.api
import cats.{ Functor, Monad, MonadError }
import io.branchtalk.shared.model.{ CodePosition, CommonError }
-import sttp.tapir._
+import sttp.tapir.*
import sttp.tapir.server.ServerEndpoint
trait Authorize[F[_], Auth, Out] {
@@ -11,7 +11,7 @@ trait Authorize[F[_], Auth, Out] {
}
object Authorize {
- implicit def functor[F[_]: Functor, Auth]: Functor[Authorize[F, Auth, *]] = new Functor[Authorize[F, Auth, *]] {
+ given functor[F[_]: Functor, Auth]: Functor[Authorize[F, Auth, *]] = new Functor[Authorize[F, Auth, *]] {
override def map[A, B](fa: Authorize[F, Auth, A])(f: A => B): Authorize[F, Auth, B] =
(auth: Auth, requiredPermissions: RequiredPermissions) => fa.authorize(auth, requiredPermissions).map(f)
}
@@ -23,11 +23,11 @@ trait AuthorizeWithOwnership[F[_], Auth, Owner, Out] {
}
object AuthorizeWithOwnership {
- implicit def functor[F[_]: Functor, Auth, Owner]: Functor[AuthorizeWithOwnership[F, Auth, Owner, *]] =
+ given functor[F[_]: Functor, Auth, Owner]: Functor[AuthorizeWithOwnership[F, Auth, Owner, *]] =
new Functor[AuthorizeWithOwnership[F, Auth, Owner, *]] {
override def map[A, B](
fa: AuthorizeWithOwnership[F, Auth, Owner, A]
- )(f: A => B): AuthorizeWithOwnership[F, Auth, Owner, B] =
+ )(f: A => B): AuthorizeWithOwnership[F, Auth, Owner, B] =
(auth: Auth, requiredPermissions: RequiredPermissions, owner: Owner) =>
fa.authorize(auth, requiredPermissions, owner).map(f)
}
@@ -55,41 +55,33 @@ object AuthedEndpoint {
) {
def apply(
logic: I => F[O]
- )(implicit
- F: Monad[F],
- errorHandler: ServerErrorHandler[F, E],
- authorize: Authorize[F, A, U]
- ): ServerEndpoint.Full[A, A, I, E, O, R, F] =
+ )(using Monad[F], ServerErrorHandler[F, E], Authorize[F, A, U]): ServerEndpoint.Full[A, A, I, E, O, R, F] =
buildServerEndpoint((i, _, _) => i, logic)
def withUser(
logic: (U, I) => F[O]
- )(implicit
- F: Monad[F],
- errorHandler: ServerErrorHandler[F, E],
- authorize: Authorize[F, A, U]
- ): ServerEndpoint.Full[A, A, I, E, O, R, F] =
+ )(using Monad[F], ServerErrorHandler[F, E], Authorize[F, A, U]): ServerEndpoint.Full[A, A, I, E, O, R, F] =
buildServerEndpoint((i, _, u) => (u, i), logic.tupled)
def justUser(
logic: U => F[O]
- )(implicit
- F: Monad[F],
- errorHandler: ServerErrorHandler[F, E],
- authorize: Authorize[F, A, U],
- ev: I =:= Unit
+ )(using
+ Monad[F],
+ ServerErrorHandler[F, E],
+ Authorize[F, A, U],
+ I =:= Unit
): ServerEndpoint.Full[A, A, I, E, O, R, F] =
buildServerEndpoint((_, _, u) => u, logic)
private def buildServerEndpoint[In](
input: (I, A, U) => In,
logic: In => F[O]
- )(implicit
+ )(using
F: Monad[F],
errorHandler: ServerErrorHandler[F, E],
authorize: Authorize[F, A, U]
): ServerEndpoint.Full[A, A, I, E, O, R, F] =
- endpoint.serverSecurityLogicPure(_.asRight[E]).serverLogic { auth: A => i: I =>
+ endpoint.serverSecurityLogicPure(_.asRight[E]).serverLogic { (auth: A) => (i: I) =>
for {
u <- authorize.authorize(auth, makePermissions(i))
in = input(i, auth, u)
@@ -105,50 +97,47 @@ object AuthedEndpoint {
) {
def apply(
logic: I => F[O]
- )(implicit
- F: MonadError[F, Throwable],
- errorHandler: ServerErrorHandler[F, E],
- authorize: AuthorizeWithOwnership[F, A, Owner, U],
- codePosition: CodePosition
+ )(using
+ MonadError[F, Throwable],
+ ServerErrorHandler[F, E],
+ AuthorizeWithOwnership[F, A, Owner, U],
+ CodePosition
): ServerEndpoint.Full[A, A, I, E, O, R, F] =
buildServerEndpoint((i, _, _) => i, logic)
def withUser(
logic: (U, I) => F[O]
- )(implicit
- F: MonadError[F, Throwable],
- errorHandler: ServerErrorHandler[F, E],
- authorize: AuthorizeWithOwnership[F, A, Owner, U],
- codePosition: CodePosition
+ )(using
+ MonadError[F, Throwable],
+ ServerErrorHandler[F, E],
+ AuthorizeWithOwnership[F, A, Owner, U],
+ CodePosition
): ServerEndpoint.Full[A, A, I, E, O, R, F] =
buildServerEndpoint((i, _, u) => (u, i), logic.tupled)
def justUser(
logic: U => F[O]
- )(implicit
- F: MonadError[F, Throwable],
- errorHandler: ServerErrorHandler[F, E],
- authorize: AuthorizeWithOwnership[F, A, Owner, U],
- codePosition: CodePosition,
- ev: I =:= Unit
+ )(using
+ MonadError[F, Throwable],
+ ServerErrorHandler[F, E],
+ AuthorizeWithOwnership[F, A, Owner, U],
+ CodePosition,
+ I =:= Unit
): ServerEndpoint.Full[A, A, I, E, O, R, F] =
buildServerEndpoint((_, _, u) => u, logic)
private def buildServerEndpoint[In](
input: (I, A, U) => In,
logic: In => F[O]
- )(implicit
+ )(using
F: MonadError[F, Throwable],
errorHandler: ServerErrorHandler[F, E],
authorize: AuthorizeWithOwnership[F, A, Owner, U],
codePosition: CodePosition
): ServerEndpoint.Full[A, A, I, E, O, R, F] =
- endpoint.serverSecurityLogicPure(_.asRight[E]).serverLogic { auth: A => i: I =>
+ endpoint.serverSecurityLogicPure(_.asRight[E]).serverLogic { (auth: A) => (i: I) =>
for {
- owner <- ownership(i).handleErrorWith { _ =>
- (CommonError.InsufficientPermissions("Ownership was not confirmed", codePosition): Throwable)
- .raiseError[F, Owner]
- }
+ owner <- ownership(i).orRaise(CommonError.insufficientPermissions("Ownership was not confirmed"))
u <- authorize.authorize(auth, makePermissions(i), owner)
in = input(i, auth, u)
out <- errorHandler(logic(in))
diff --git a/modules/common-api/src/main/scala/io/branchtalk/api/credentials.scala b/modules/common-api/src/main/scala/io/branchtalk/api/credentials.scala
new file mode 100644
index 00000000..22f52afb
--- /dev/null
+++ b/modules/common-api/src/main/scala/io/branchtalk/api/credentials.scala
@@ -0,0 +1,33 @@
+package io.branchtalk.api
+
+import cats.{ Eq, Show }
+import cats.effect.Sync
+import io.branchtalk.shared.model.ParseNewtype
+import io.branchtalk.api.JsoniterSupport.*
+import io.branchtalk.api.TapirSupport.*
+
+type Username = Username.Type
+object Username extends Newtype[String] {
+
+ override inline def validate(input: String): Boolean = input.nonEmpty
+
+ def unapply(username: Username): Some[String] = Some(username.unwrap)
+ def parse[F[_]: Sync](string: String): F[Username] = ParseNewtype[F].parse[Username](string)
+
+ given Eq[Username] = unsafeMakeF[Eq](Eq[String])
+ given Show[Username] = unsafeMakeF[Show](Show[String])
+ given JsCodec[Username] = DefaultJsCodec.derived[String].asNewtypeCodec[Username]
+ given JsSchema[Username] = summonSchema[String].asNewtypeSchema[Username]
+}
+
+type Password = Password.Type
+object Password extends Newtype[Array[Byte]] {
+
+ override inline def validate(input: Array[Byte]): Boolean = input.nonEmpty
+
+ def unapply(password: Password): Some[Array[Byte]] = Some(password.unwrap)
+ def parse[F[_]: Sync](array: Array[Byte]): F[Password] = ParseNewtype[F].parse[Password](array)
+
+ given JsCodec[Password] = DefaultJsCodec.derived[Array[Byte]].asNewtypeCodec[Password]
+ given JsSchema[Password] = summonSchema[Array[Byte]].asNewtypeSchema[Password]
+}
diff --git a/modules/common-api/src/main/scala/io/branchtalk/api/ids.scala b/modules/common-api/src/main/scala/io/branchtalk/api/ids.scala
new file mode 100644
index 00000000..5f6e2b00
--- /dev/null
+++ b/modules/common-api/src/main/scala/io/branchtalk/api/ids.scala
@@ -0,0 +1,47 @@
+package io.branchtalk.api
+
+import cats.{ Eq, Show }
+import cats.effect.Sync
+import io.branchtalk.shared.model.{ ParseNewtype, UUID }
+import io.branchtalk.api.JsoniterSupport.*
+import io.branchtalk.api.TapirSupport.*
+
+type SessionID = SessionID.Type
+object SessionID extends Newtype[UUID] {
+
+ def unapply(sessionID: SessionID): Some[UUID] = Some(sessionID.unwrap)
+ def parse[F[_]: Sync](string: String)(using UUID.Generator): F[SessionID] = UUID.parse[F](string).map(unsafeMake)
+
+ given Eq[SessionID] = unsafeMakeF[Eq](Eq.fromUniversalEquals[UUID])
+ given Show[SessionID] = unsafeMakeF[Show](Show[UUID])
+ given JsCodec[SessionID] = DefaultJsCodec.derived[UUID].asNewtypeCodec[SessionID]
+ given JsSchema[SessionID] = summonSchema[UUID].asNewtypeSchema[SessionID]
+}
+
+type UserID = UserID.Type
+object UserID extends Newtype[UUID] {
+
+ val empty: UserID = unsafeMake(UUID.empty)
+
+ def unapply(userID: UserID): Some[UUID] = Some(userID.unwrap)
+ def parse[F[_]: Sync](string: String)(using UUID.Generator): F[UserID] = UUID.parse[F](string).map(unsafeMake)
+
+ given Eq[UserID] = unsafeMakeF[Eq](Eq.fromUniversalEquals[UUID])
+ given Show[UserID] = unsafeMakeF[Show](Show[UUID])
+ given JsCodec[UserID] = DefaultJsCodec.derived[UUID].asNewtypeCodec[UserID]
+ given JsSchema[UserID] = summonSchema[UUID].asNewtypeSchema[UserID]
+}
+
+type ChannelID = ChannelID.Type
+object ChannelID extends Newtype[UUID] {
+
+ val empty: ChannelID = unsafeMake(UUID.empty)
+
+ def unapply(channelID: ChannelID): Some[UUID] = Some(channelID.unwrap)
+ def parse[F[_]: Sync](string: String)(using UUID.Generator): F[ChannelID] = UUID.parse[F](string).map(unsafeMake)
+
+ given Eq[ChannelID] = unsafeMakeF[Eq](Eq.fromUniversalEquals[UUID])
+ given Show[ChannelID] = unsafeMakeF[Show](Show[UUID])
+ given JsCodec[ChannelID] = DefaultJsCodec.derived[UUID].asNewtypeCodec[ChannelID]
+ given JsSchema[ChannelID] = summonSchema[UUID].asNewtypeSchema[ChannelID]
+}
diff --git a/modules/common-api/src/main/scala/io/branchtalk/api/package.scala b/modules/common-api/src/main/scala/io/branchtalk/api/package.scala
deleted file mode 100644
index b97cd15d..00000000
--- a/modules/common-api/src/main/scala/io/branchtalk/api/package.scala
+++ /dev/null
@@ -1,125 +0,0 @@
-package io.branchtalk
-
-import cats.{ Eq, Show }
-import cats.effect.Sync
-import com.github.plokhotnyuk.jsoniter_scala.macros._
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.collection.NonEmpty
-import eu.timepit.refined.numeric.{ NonNegative, Positive }
-import eu.timepit.refined.types.string.NonEmptyString
-import io.branchtalk.shared.model._
-import io.branchtalk.api.JsoniterSupport._
-import io.branchtalk.api.TapirSupport._
-import io.estatico.newtype.macros.newtype
-import io.estatico.newtype.ops._
-
-// scalastyle:off number.of.methods
-package object api {
-
- // API definitions and instances
-
- @newtype final case class SessionID(uuid: UUID)
- object SessionID {
- def unapply(sessionID: SessionID): Some[UUID] = Some(sessionID.uuid)
- def parse[F[_]: Sync](string: String)(implicit uuidGenerator: UUIDGenerator): F[SessionID] =
- UUID.parse[F](string).map(SessionID(_))
-
- implicit val eq: Eq[SessionID] = Eq[UUID].coerce
- implicit val show: Show[SessionID] = Show[UUID].coerce
- @SuppressWarnings(Array("org.wartremover.warts.Null"))
- implicit val codec: JsCodec[SessionID] = summonCodec[UUID](JsonCodecMaker.make).asNewtype[SessionID]
- implicit val schema: JsSchema[SessionID] = summonSchema[UUID].asNewtype[SessionID]
- }
-
- @newtype final case class UserID(uuid: UUID)
- object UserID {
- def unapply(userID: UserID): Some[UUID] = Some(userID.uuid)
- def parse[F[_]: Sync](string: String)(implicit uuidGenerator: UUIDGenerator): F[UserID] =
- UUID.parse[F](string).map(UserID(_))
-
- val empty: UserID = UserID(java.util.UUID.fromString("00000000-0000-0000-0000-000000000000"))
-
- implicit val eq: Eq[UserID] = Eq[UUID].coerce
- implicit val show: Show[UserID] = Show[UUID].coerce
- @SuppressWarnings(Array("org.wartremover.warts.Null"))
- implicit val codec: JsCodec[UserID] = summonCodec[UUID](JsonCodecMaker.make).asNewtype[UserID]
- implicit val schema: JsSchema[UserID] = summonSchema[UUID].asNewtype[UserID]
- }
-
- @newtype final case class ChannelID(uuid: UUID)
- object ChannelID {
- def unapply(channelID: ChannelID): Some[UUID] = Some(channelID.uuid)
- def parse[F[_]: Sync](string: String)(implicit uuidGenerator: UUIDGenerator): F[ChannelID] =
- UUID.parse[F](string).map(ChannelID(_))
-
- implicit val eq: Eq[ChannelID] = Eq[UUID].coerce
- implicit val show: Show[ChannelID] = Show[UUID].coerce
- @SuppressWarnings(Array("org.wartremover.warts.Null"))
- implicit val codec: JsCodec[ChannelID] = summonCodec[UUID](JsonCodecMaker.make).asNewtype[ChannelID]
- implicit val schema: JsSchema[ChannelID] = summonSchema[UUID].asNewtype[ChannelID]
- }
-
- @newtype final case class Username(nonEmptyString: NonEmptyString)
- object Username {
- def unapply(username: Username): Some[NonEmptyString] = Some(username.nonEmptyString)
- def parse[F[_]: Sync](string: String): F[Username] =
- ParseRefined[F].parse[NonEmpty](string).map(Username(_))
-
- @SuppressWarnings(Array("org.wartremover.warts.Null"))
- implicit val codec: JsCodec[Username] =
- summonCodec[String](JsonCodecMaker.make).refine[NonEmpty].asNewtype[Username]
- implicit val schema: JsSchema[Username] =
- summonSchema[String Refined NonEmpty].asNewtype[Username]
- }
-
- @newtype final case class Password(nonEmptyBytes: Array[Byte] Refined NonEmpty)
- object Password {
- def unapply(password: Password): Some[Array[Byte] Refined NonEmpty] = Some(password.nonEmptyBytes)
- def parse[F[_]: Sync](bytes: Array[Byte]): F[Password] =
- ParseRefined[F].parse[NonEmpty](bytes).map(Password(_))
-
- @SuppressWarnings(Array("org.wartremover.warts.All")) // macros
- implicit val codec: JsCodec[Password] =
- summonCodec[String](JsonCodecMaker.make).map(_.getBytes)(new String(_)).refine[NonEmpty].asNewtype[Password]
- implicit val schema: JsSchema[Password] =
- summonSchema[Array[Byte] Refined NonEmpty].asNewtype[Password]
- }
-
- @newtype final case class PaginationOffset(nonNegativeLong: Long Refined NonNegative)
- object PaginationOffset {
- def unapply(offset: PaginationOffset): Some[Long Refined NonNegative] = Some(offset.nonNegativeLong)
- def parse[F[_]: Sync](long: Long): F[PaginationOffset] =
- ParseRefined[F].parse[NonNegative](long).map(PaginationOffset(_))
-
- implicit val codec: JsCodec[PaginationOffset] =
- summonCodec[Long](JsonCodecMaker.make).refine[NonNegative].asNewtype[PaginationOffset]
- implicit val param: Param[PaginationOffset] =
- summonParam[Long Refined NonNegative].map(PaginationOffset(_))(_.nonNegativeLong)
- implicit val schema: JsSchema[PaginationOffset] =
- summonSchema[Long Refined NonNegative].asNewtype[PaginationOffset]
- }
-
- @newtype final case class PaginationLimit(positiveInt: Int Refined Positive)
- object PaginationLimit {
- def unapply(limit: PaginationLimit): Some[Int Refined Positive] = Some(limit.positiveInt)
- def parse[F[_]: Sync](int: Int): F[PaginationLimit] =
- ParseRefined[F].parse[Positive](int).map(PaginationLimit(_))
-
- implicit val codec: JsCodec[PaginationLimit] =
- summonCodec[Int](JsonCodecMaker.make).refine[Positive].asNewtype[PaginationLimit]
- implicit val param: Param[PaginationLimit] =
- summonParam[Int Refined Positive].map(PaginationLimit(_))(_.positiveInt)
- implicit val schema: JsSchema[PaginationLimit] =
- summonSchema[Int Refined Positive].asNewtype[PaginationLimit]
- }
-
- @newtype final case class PaginationHasNext(bool: Boolean)
- object PaginationHasNext {
- def unapply(hasNext: PaginationHasNext): Some[Boolean] = Some(hasNext.bool)
-
- implicit val codec: JsCodec[PaginationHasNext] =
- summonCodec[Boolean](JsonCodecMaker.make).asNewtype[PaginationHasNext]
- implicit val schema: JsSchema[PaginationHasNext] =
- summonSchema[Boolean].asNewtype[PaginationHasNext]
- }
-}
diff --git a/modules/common-infrastructure/src/it/scala/io/branchtalk/shared/infrastructure/TestKafkaEventBusConfig.scala b/modules/common-infrastructure/src/it/scala/io/branchtalk/shared/infrastructure/TestKafkaEventBusConfig.scala
deleted file mode 100644
index 68f02462..00000000
--- a/modules/common-infrastructure/src/it/scala/io/branchtalk/shared/infrastructure/TestKafkaEventBusConfig.scala
+++ /dev/null
@@ -1,21 +0,0 @@
-package io.branchtalk.shared.infrastructure
-
-import cats.data.NonEmptyList
-import eu.timepit.refined.collection.NonEmpty
-import eu.timepit.refined.refineV
-import io.branchtalk.shared.infrastructure.PureconfigSupport._
-import io.scalaland.catnip.Semi
-import io.scalaland.chimney.dsl._
-
-@Semi(ConfigReader) final case class TestKafkaEventBusConfig(
- servers: NonEmptyList[Server],
- topicPrefix: Topic,
- cache: Server
-) {
-
- def topic(generatedSuffix: String): Topic =
- Topic(refineV[NonEmpty](topicPrefix.nonEmptyString.value + generatedSuffix).getOrElse(???))
-
- def toKafkaEventBusConfig(generatedSuffix: String): KafkaEventBusConfig =
- this.into[KafkaEventBusConfig].withFieldConst(_.topic, topic(generatedSuffix)).transform
-}
diff --git a/modules/common-infrastructure/src/it/scala/io/branchtalk/shared/infrastructure/TestPostgresConfig.scala b/modules/common-infrastructure/src/it/scala/io/branchtalk/shared/infrastructure/TestPostgresConfig.scala
deleted file mode 100644
index f801b9ea..00000000
--- a/modules/common-infrastructure/src/it/scala/io/branchtalk/shared/infrastructure/TestPostgresConfig.scala
+++ /dev/null
@@ -1,32 +0,0 @@
-package io.branchtalk.shared.infrastructure
-
-import eu.timepit.refined.collection.NonEmpty
-import eu.timepit.refined.refineV
-import io.scalaland.catnip.Semi
-import io.scalaland.chimney.dsl._
-import pureconfig.ConfigReader
-
-@Semi(ConfigReader) final case class TestPostgresConfig(
- url: DatabaseURL,
- rootPassword: DatabasePassword,
- usernamePrefix: DatabaseUsername,
- password: DatabasePassword,
- schemaPrefix: DatabaseSchema,
- domain: DatabaseDomain,
- connectionPool: DatabaseConnectionPool
-) {
-
- def username(generatedSuffix: String): DatabaseUsername =
- DatabaseUsername(refineV[NonEmpty](usernamePrefix.nonEmptyString.value + generatedSuffix).getOrElse(???))
- def schema(generatedSuffix: String): DatabaseSchema =
- DatabaseSchema(refineV[NonEmpty](schemaPrefix.nonEmptyString.value + generatedSuffix).getOrElse(???))
- def migrationOnStart: DatabaseMigrationOnStart = DatabaseMigrationOnStart(true)
-
- def toPostgresConfig(generatedSuffix: String): PostgresConfig =
- this
- .into[PostgresConfig]
- .withFieldConst(_.username, username(generatedSuffix))
- .withFieldConst(_.schema, schema(generatedSuffix))
- .withFieldConst(_.migrationOnStart, migrationOnStart)
- .transform
-}
diff --git a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/Cache.scala b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/Cache.scala
index 10aa1607..6252618f 100644
--- a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/Cache.scala
+++ b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/Cache.scala
@@ -8,7 +8,8 @@ import dev.profunktor.redis4cats.effect.Log
import dev.profunktor.redis4cats.{ Redis, RedisCommands }
import fs2.kafka.{ Headers, Serializer }
import fs2.{ Pipe, Stream }
-import io.branchtalk.shared.model.{ Logger, branchtalkCharset }
+import io.branchtalk.logging.Logger
+import io.branchtalk.shared.model.branchtalkCharset
import io.lettuce.core.codec.{ RedisCodec => JRedisCodec }
import scala.util.control.NoStackTrace
@@ -18,11 +19,10 @@ abstract class Cache[F[_]: Sync, K, V] {
def apply(key: K)(value: F[V]): F[(K, V)]
- final private case object EmptyStream extends Exception with NoStackTrace
+ private case object EmptyStream extends Exception, NoStackTrace
- @SuppressWarnings(Array("org.wartremover.warts.Throw")) // will be handled just after use
private def unliftPipe[I](pipe: Pipe[F, I, V]): I => F[V] =
- i => Stream(i).through(pipe).compile.last.map(_.getOrElse(throw EmptyStream))
+ i => Stream(i).through(pipe).compile.last.flatMap(_.fold((EmptyStream: Throwable).raiseError[F, V])(_.pure[F]))
def piped[I](key: I => K, pipe: Pipe[F, I, V]): Pipe[F, I, V] =
(_: Stream[F, I]).flatMap(i => Stream.eval(apply(key(i))(unliftPipe(pipe)(i)))).map(_._2).handleErrorWith {
@@ -32,7 +32,7 @@ abstract class Cache[F[_]: Sync, K, V] {
}
object Cache {
- def fromRedis[F[_]: Sync, K, V](redis: RedisCommands[F, K, V]): Cache[F, K, V] = new Cache[F, K, V] {
+ def fromRedis[F[_]: Sync, K, V](redis: RedisCommands[F, K, V]): Cache[F, K, V] = new {
override def apply(key: K)(valueF: F[V]): F[(K, V)] = redis.get(key).flatMap {
case Some(value) => (key -> value).pure[F]
@@ -41,23 +41,20 @@ object Cache {
}
def fromConfigs[F[_]: Async: Dispatcher, Event: Serializer[F, *]: SafeDeserializer[F, *]](
- busConfig: KafkaEventBusConfig
+ busConfig: KafkaEventBus.BusConfig
): Resource[F, Cache[F, String, Event]] =
for {
logger <- Resource.eval(Logger.fromClass[F](classOf[Cache[F, String, Event]]))
- implicit0(log: Log[F]) = new Log[F] {
+ given Log[F] = new Log[F] {
override def debug(msg: => String): F[Unit] = logger.debug(msg)
override def error(msg: => String): F[Unit] = logger.error(msg)
override def info(msg: => String): F[Unit] = logger.info(msg)
}
- redis <- Redis[F].simple(
- show"redis://${busConfig.cache.host.value}:${busConfig.cache.port.value}",
- prepareCodec[F, Event](busConfig.topic.nonEmptyString.value)
- )
+ redis <- Redis[F].simple(show"redis://${busConfig.cache}", prepareCodec[F, Event](busConfig.topic))
} yield fromRedis(redis)
private def prepareCodec[F[_]: Sync: Dispatcher, Event: Serializer[F, *]: SafeDeserializer[F, *]](
- topic: String
+ topic: KafkaEventBus.Topic
): RedisCodec[String, Event] = RedisCodec(
new JRedisCodec[String, Event] {
override def decodeKey(bytes: ByteBuffer): String = new String(bytes.array(), branchtalkCharset)
@@ -68,21 +65,21 @@ object Cache {
override def decodeValue(bytes: ByteBuffer): Event =
if (bytes.hasArray) {
SafeDeserializer[F, Event]
- .deserialize(topic, Headers.empty, bytes.array())
+ .deserialize(topic.unwrap, Headers.empty, bytes.array())
.flatMap {
case Left(error) => new Exception(error.toString).raiseError[F, Event]
case Right(value) => value.pure[F]
}
- .pipe(implicitly[Dispatcher[F]].unsafeRunSync(_))
- } else null.asInstanceOf[Event] // scalastyle:ignore null
+ .pipe(summon[Dispatcher[F]].unsafeRunSync(_))
+ } else null.asInstanceOf[Event]
override def encodeKey(key: String): ByteBuffer = ByteBuffer.wrap(key.getBytes(branchtalkCharset))
override def encodeValue(value: Event): ByteBuffer =
Serializer[F, Event]
- .serialize(topic, Headers.empty, value)
+ .serialize(topic.unwrap, Headers.empty, value)
.map(ByteBuffer.wrap)
- .pipe(implicitly[Dispatcher[F]].unsafeRunSync(_))
+ .pipe(summon[Dispatcher[F]].unsafeRunSync(_))
}
)
}
diff --git a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/ConsumerStream.scala b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/ConsumerStream.scala
index acd6efb0..2b7e44d3 100644
--- a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/ConsumerStream.scala
+++ b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/ConsumerStream.scala
@@ -1,19 +1,21 @@
package io.branchtalk.shared.infrastructure
import cats.effect.Async
-import fs2.{ io => _, _ }
+import fs2.{ io => _, * }
import fs2.kafka.{ ProducerResult, Serializer }
-import io.branchtalk.shared.model.{ Logger, UUID }
+import io.branchtalk.logging.Logger
+import io.branchtalk.shared.model.UUID
+// TODO: move to KafkaEventBus
final class ConsumerStream[F[_], Event](
- consumer: EventBusConsumer[F, Event],
- committer: EventBusCommitter[F]
+ consumer: KafkaEventBus.Consumer[F, Event],
+ committer: KafkaEventBus.Committer[F]
) {
// Runs pipe (projections) on events and commit them once they are processed.
def runThrough[B](
logger: Logger[F]
- )(f: Pipe[F, (String, Event), B])(implicit F: Async[F]): StreamRunner[F] = StreamRunner[F, Unit] {
+ )(f: Pipe[F, (String, Event), B])(using Async[F]): StreamRunner[F] = StreamRunner[F, Unit] {
consumer
.flatMap { event =>
Stream(s"${event.record.topic}:${event.record.offset.toString}" -> event.record.value)
@@ -28,15 +30,15 @@ final class ConsumerStream[F[_], Event](
def runCachedThrough[B](
logger: Logger[F],
cache: Cache[F, String, B]
- )(f: Pipe[F, (String, Event), B])(implicit F: Async[F]): StreamRunner[F] =
+ )(f: Pipe[F, (String, Event), B])(using Async[F]): StreamRunner[F] =
runThrough(logger)(cache.piped(_._1, f))
}
object ConsumerStream {
- type Factory[F[_], Event] = KafkaEventConsumerConfig => ConsumerStream[F, Event]
+ type Factory[F[_], Event] = KafkaEventBus.ConsumerConfig => ConsumerStream[F, Event]
def fromConfigs[F[_]: Async, Event: Serializer[F, *]: SafeDeserializer[F, *]](
- busConfig: KafkaEventBusConfig
+ busConfig: KafkaEventBus.BusConfig
): Factory[F, Event] =
consumerCfg =>
new ConsumerStream(
@@ -46,6 +48,5 @@ object ConsumerStream {
def noID[F[_], A, B]: Pipe[F, (A, B), B] = _.map(_._2)
- def produced[F[_], A]: Pipe[F, ProducerResult[Unit, UUID, A], A] =
- _.flatMap(pr => Stream(pr.records.map(_._1.value).toList: _*))
+ def produced[F[_], A]: Pipe[F, ProducerResult[UUID, A], A] = _.flatMap(pr => Stream(pr.map(_._1.value).toList: _*))
}
diff --git a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/DatabasePassword.scala b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/DatabasePassword.scala
deleted file mode 100644
index 4eabdb12..00000000
--- a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/DatabasePassword.scala
+++ /dev/null
@@ -1,17 +0,0 @@
-package io.branchtalk.shared.infrastructure
-
-import cats.Show
-import eu.timepit.refined.types.string.NonEmptyString
-import io.branchtalk.shared.infrastructure.PureconfigSupport._
-import io.branchtalk.shared.model._
-
-// not @newtype to allow overriding toString
-final case class DatabasePassword(nonEmptyString: NonEmptyString) {
- override def toString: String = "[PASSWORD]"
-}
-object DatabasePassword {
- def unapply(databasePassword: DatabasePassword): Some[NonEmptyString] = Some(databasePassword.nonEmptyString)
-
- implicit val configReader: ConfigReader[DatabasePassword] = ConfigReader[NonEmptyString].map(DatabasePassword(_))
- implicit val show: Show[DatabasePassword] = Show.wrap(_.toString)
-}
diff --git a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/DomainConfig.scala b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/DomainConfig.scala
deleted file mode 100644
index f1182f18..00000000
--- a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/DomainConfig.scala
+++ /dev/null
@@ -1,18 +0,0 @@
-package io.branchtalk.shared.infrastructure
-
-import io.scalaland.catnip.Semi
-import io.branchtalk.shared.infrastructure.PureconfigSupport._
-import io.branchtalk.shared.model.ShowPretty
-
-@Semi(ConfigReader, ShowPretty) final case class DomainConfig(
- name: DomainName,
- databaseReads: PostgresConfig,
- databaseWrites: PostgresConfig,
- publishedEventBus: KafkaEventBusConfig,
- internalEventBus: KafkaEventBusConfig,
- consumers: Map[String, KafkaEventConsumerConfig]
-) {
-
- // assumes that each config has to have this field
- def internalConsumer: KafkaEventConsumerConfig = consumers("internal")
-}
diff --git a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/DomainModule.scala b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/DomainModule.scala
index edf771b2..4e43befa 100644
--- a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/DomainModule.scala
+++ b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/DomainModule.scala
@@ -1,50 +1,43 @@
package io.branchtalk.shared.infrastructure
+import cats.Show
import cats.effect.std.Dispatcher
import cats.effect.{ Async, Resource }
import com.sksamuel.avro4s.{ Decoder, Encoder, SchemaFor }
import doobie.util.transactor.Transactor
-import io.branchtalk.shared.infrastructure.KafkaSerialization._
+import io.branchtalk.logging.Logger
+import io.branchtalk.shared.infrastructure.KafkaSerialization.{ *, given }
+import io.branchtalk.shared.infrastructure.PureconfigSupport.{ *, given }
+import io.branchtalk.shared.model.ShowPretty
import io.prometheus.client.CollectorRegistry
// Utilities for connecting to database and events buses through Resources.
-final case class ReadsInfrastructure[F[_], Event](
- transactor: Transactor[F],
- consumer: KafkaEventConsumerConfig => ConsumerStream[F, Event]
-)
-
-final case class WritesInfrastructure[F[_], Event, InternalEvent](
- transactor: Transactor[F],
- internalProducer: EventBusProducer[F, InternalEvent],
- internalConsumerStream: ConsumerStream[F, InternalEvent],
- producer: EventBusProducer[F, Event],
- consumerStream: ConsumerStream.Factory[F, Event],
- cache: Cache[F, String, Event]
-)
final class DomainModule[Event: Encoder: Decoder: SchemaFor, InternalEvent: Encoder: Decoder: SchemaFor] {
def setupReads[F[_]: Async](
- domainConfig: DomainConfig,
+ domainConfig: DomainModule.Config,
+ logger: Logger[F],
registry: CollectorRegistry
- ): Resource[F, ReadsInfrastructure[F, Event]] =
+ ): Resource[F, Reads.Infrastructure[F, Event]] =
for {
- transactor <- new PostgresDatabase(domainConfig.databaseReads).transactor(registry)
+ transactor <- new PostgresDatabase(domainConfig.databaseReads).transactor(logger, registry)
consumerStreamBuilder = ConsumerStream.fromConfigs[F, Event](domainConfig.publishedEventBus)
- } yield ReadsInfrastructure(transactor, consumerStreamBuilder)
+ } yield Reads.Infrastructure(transactor, consumerStreamBuilder)
def setupWrites[F[_]: Async: Dispatcher](
- domainConfig: DomainConfig,
+ domainConfig: DomainModule.Config,
+ logger: Logger[F],
registry: CollectorRegistry
- ): Resource[F, WritesInfrastructure[F, Event, InternalEvent]] =
+ ): Resource[F, Writes.Infrastructure[F, Event, InternalEvent]] =
for {
- transactor <- new PostgresDatabase(domainConfig.databaseWrites).transactor(registry)
+ transactor <- new PostgresDatabase(domainConfig.databaseWrites).transactor(logger, registry)
internalProducer = KafkaEventBus.producer[F, InternalEvent](domainConfig.internalEventBus)
internalConsumerStream = ConsumerStream.fromConfigs[F, InternalEvent](domainConfig.internalEventBus)
producer = KafkaEventBus.producer[F, Event](domainConfig.publishedEventBus)
consumerStream = ConsumerStream.fromConfigs[F, Event](domainConfig.publishedEventBus)
cache <- Cache.fromConfigs[F, Event](domainConfig.internalEventBus)
- } yield WritesInfrastructure(
+ } yield Writes.Infrastructure(
transactor,
internalProducer,
internalConsumerStream(domainConfig.internalConsumer),
@@ -59,4 +52,29 @@ object DomainModule {
Event,
InternalEvent
] = new DomainModule[Event, InternalEvent]
+
+ type Name = Name.Type
+ object Name extends Newtype[String] {
+
+ override inline def validate(input: String): Boolean = input.nonEmpty
+
+ def unapply(domainName: Name): Some[String] = Some(domainName.unwrap)
+
+ given ConfigReader[Name] = ConfigReader[String].emapString("Name")(make)
+ given Show[Name] = unsafeMakeF[Show](Show[String])
+ }
+
+ final case class Config(
+ name: Name,
+ databaseReads: PostgresDatabase.Config,
+ databaseWrites: PostgresDatabase.Config,
+ publishedEventBus: KafkaEventBus.BusConfig,
+ internalEventBus: KafkaEventBus.BusConfig,
+ consumers: Map[String, KafkaEventBus.ConsumerConfig]
+ ) derives ConfigReader,
+ ShowPretty {
+
+ // assumes that each config has to have this field
+ def internalConsumer: KafkaEventBus.ConsumerConfig = consumers("internal")
+ }
}
diff --git a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/DoobieSupport.scala b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/DoobieSupport.scala
index 4d1fd815..c2c6d787 100644
--- a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/DoobieSupport.scala
+++ b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/DoobieSupport.scala
@@ -1,82 +1,88 @@
package io.branchtalk.shared.infrastructure
import cats.effect.{ Sync, SyncIO }
-import com.typesafe.scalalogging.Logger
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.numeric.{ NonNegative, Positive }
-import io.branchtalk.shared.model._
-import io.estatico.newtype.Coercible
+import doobie.util.log
+import io.branchtalk.logging.Logger
+import io.branchtalk.shared.model.*
import org.tpolecat.typename.TypeName
-// Allows `import DoobieSupport._` instead of... a lot of imports.
-// Additionally provides support for a few useful but missing features.
+// Allows `import DoobieSupport.*` instead of... a lot of imports.
+// Additionally, provides support for a few useful but missing features.
object DoobieSupport
- extends doobie.Aliases // basic functionalities
- with doobie.hi.Modules
- with doobie.syntax.AllSyntax
- with doobie.free.Modules
- with doobie.free.Types
- with doobie.free.Instances
- with doobie.postgres.Instances // postgres extensions (without postgis)
- with doobie.postgres.hi.Modules
- with doobie.postgres.free.Modules
- with doobie.postgres.free.Types
- with doobie.postgres.free.Instances
- with doobie.postgres.syntax.ToPostgresMonadErrorOps
- with doobie.postgres.syntax.ToFragmentOps
- with doobie.postgres.syntax.ToPostgresExplainOps
- with doobie.refined.Instances // refined types
- with doobie.util.meta.MetaConstructors // Java Time extensions
- with doobie.util.meta.TimeMetaInstances {
+// basic functionalities
+ extends doobie.Aliases,
+ doobie.hi.Modules,
+ doobie.syntax.AllSyntax,
+ doobie.free.Modules,
+ doobie.free.Types,
+ doobie.free.Instances,
+ // postgres extensions (without postgis)
+ doobie.postgres.Instances,
+ doobie.postgres.hi.Modules,
+ doobie.postgres.free.Modules,
+ doobie.postgres.free.Types,
+ doobie.postgres.free.Instances,
+ doobie.postgres.syntax.ToPostgresMonadErrorOps,
+ doobie.postgres.syntax.ToFragmentOps,
+ doobie.postgres.syntax.ToPostgresExplainOps,
+ // Java Time extensions
+ doobie.util.meta.MetaConstructors,
+ doobie.util.meta.TimeMetaInstances {
// enumeratum automatic support
- implicit def enumeratumMeta[A <: enumeratum.EnumEntry](implicit
- `enum`: enumeratum.Enum[A],
- typeTag: TypeName[A]
- ): Meta[A] =
- Meta[String].timap(`enum`.withNameInsensitive)(_.entryName)
+ export enumeratum.Doobie.meta as enumeraturmMeta
// newtype automatic support
- implicit def coercibleMeta[R, N](implicit ev: Coercible[Meta[R], Meta[N]], R: Meta[R]): Meta[N] = ev(R)
+ export neotype.interop.doobie.{
+ newtypeArrayGet,
+ newtypeArrayPut,
+ newtypeGet,
+ newtypePut,
+ subtypeArrayGet,
+ subtypeArrayPut,
+ subtypeGet,
+ subtypePut
+ }
- implicit def idArrayMeta[E](implicit
- to: Coercible[Set[UUID], Set[ID[E]]],
- from: Coercible[Set[ID[E]], Set[UUID]]
- ): Meta[Set[ID[E]]] =
- unliftedUUIDArrayType.imap[Set[ID[E]]](arr => to(arr.toSet))(set => from(set).toArray)
+ given [E]: Meta[ID[E]] =
+ ID.unsafeMakeF[Meta, E](Meta[UUID])
- // handle updateable
+ given [E]: Meta[Set[ID[E]]] =
+ ID.unsafeMakeF[[A] =>> Meta[Set[A]], E](unliftedUUIDArrayType.imap[Set[UUID]](_.toSet)(_.toArray))
+
+ given Meta[CreationTime] = CreationTime.unsafeMakeF[Meta](JavaOffsetDateTimeMeta)
- implicit class DoobieUpdatableOps[A](private val updatable: Updatable[A]) extends AnyVal {
+ given Meta[ModificationTime] = ModificationTime.unsafeMakeF[Meta](JavaOffsetDateTimeMeta)
+
+ // handle updateable
- def toUpdateFragment(columnName: Fragment)(implicit meta: Put[A]): Option[Fragment] =
+ extension [A](updatable: Updatable[A]) {
+ def toUpdateFragment(columnName: Fragment)(using Put[A]): Option[Fragment] =
updatable.fold(value => (columnName ++ fr" = ${value}").some, none[Fragment])
}
- implicit class DoobieOptionUpdatableOps[A](private val updatable: OptionUpdatable[A]) extends AnyVal {
-
- def toUpdateFragment(columnName: Fragment)(implicit meta: Put[A]): Option[Fragment] =
+ extension [A](updatable: OptionUpdatable[A]) {
+ def toUpdateFragment(columnName: Fragment)(using Put[A]): Option[Fragment] =
updatable.fold(value => (columnName ++ fr"= ${value}").some, (columnName ++ fr"= null").some, none[Fragment])
}
- implicit class FragmentOps(private val fragment: Fragment) extends AnyVal {
-
- def exists(implicit logHandler: LogHandler): ConnectionIO[Boolean] =
- (fr"SELECT EXISTS(" ++ fragment ++ fr")").query[Boolean].unique
+ extension (fragment: Fragment) {
+ def exists(label: String): ConnectionIO[Boolean] =
+ (fr"SELECT EXISTS(" ++ fragment ++ fr")").queryWithLabel[Boolean](label).unique
- def paginate[Entity: Read](offset: Long Refined NonNegative, limit: Int Refined Positive)(implicit
- logHandler: LogHandler
+ def paginate[Entity: Read](
+ offset: Paginated.Offset,
+ limit: Paginated.Limit,
+ label: String
): ConnectionIO[Paginated[Entity]] = {
- val o = offset.value
- val l = limit.value
+ val o: Long = offset.unwrap
+ val l: Int = limit.unwrap
// limit 1 entity more than returned to check if there is a next page in pagination
- (fragment ++ fr"LIMIT ${l + 1} OFFSET ${o}").query[Entity].to[List].map { entities =>
- val result = entities.take(l)
- val nextOffset =
- if (entities.sizeCompare(l) <= 0) None
- else ParseRefined[SyncIO].parse[NonNegative](o + l).attempt.unsafeRunSync().toOption
+ (fragment ++ fr"LIMIT ${l + 1} OFFSET ${o}").queryWithLabel[Entity](label).to[List].map { entities =>
+ val result = entities.take(l)
+ val nextOffset = if (entities.sizeIs <= l) None else Paginated.Offset.make(o + l).toOption
Paginated(result, nextOffset)
}
}
@@ -84,47 +90,43 @@ object DoobieSupport
// handle errors
- implicit class QueryOps[A](private val query: Query0[A]) extends AnyVal {
-
- def failNotFound(entity: String, id: ID[_])(implicit codePosition: CodePosition): ConnectionIO[A] =
- query.unique.handleErrorWith { _ =>
- Sync[ConnectionIO].raiseError(CommonError.NotFound(entity, id, codePosition))
- }
+ extension [A](query: Query0[A]) {
+ @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
+ def failNotFound[Entity](entity: String, id: ID[Entity])(using CodePosition): ConnectionIO[A] =
+ query.unique.orRaise(CommonError.notFound(entity, id.asInstanceOf[ID[Any]]))
}
// log results
- def doobieLogger(clazz: Class[_]): LogHandler = {
- val logger = Logger(clazz)
- LogHandler {
- case doobie.util.log.Success(sql, _, exec, processing) =>
- logger.trace(
- s"""SQL succeeded:
- |${sql}
- |execution: ${exec.toMillis.toString} ms
- |processing: ${processing.toMillis.toString} ms
- |total: ${(exec + processing).toMillis.toString} ms""".stripMargin
- )
- case doobie.util.log.ExecFailure(sql, _, exec, failure) =>
- logger.error(
- s"""SQL failed at execution:
- |${sql}
- |failure cause:
- |${failure.getMessage} ms
- |execution: ${exec.toMillis.toString} ms""".stripMargin,
- failure
- )
- case doobie.util.log.ProcessingFailure(sql, _, exec, processing, failure) =>
- logger.error(
- s"""SQL failed at processing:
- |${sql}
- |failure cause:
- |${failure.getMessage}
- |execution: ${exec.toMillis.toString} ms
- |processing: ${processing.toMillis.toString} ms
- |total: ${(exec + processing).toMillis.toString} ms""".stripMargin,
- failure
- )
- }
+ def doobieLogger[F[_]](logger: Logger[F]): LogHandler[F] = {
+ case doobie.util.log.Success(sql, _, label, exec, processing) =>
+ logger.trace(
+ s"""SQL succeeded:
+ |${label}
+ |${sql}
+ |execution: ${exec.toMillis.toString} ms
+ |processing: ${processing.toMillis.toString} ms
+ |total: ${(exec + processing).toMillis.toString} ms""".stripMargin
+ )
+ case doobie.util.log.ExecFailure(sql, _, label, exec, failure) =>
+ logger.error(failure)(
+ s"""SQL failed at execution:
+ |${label}
+ |${sql}
+ |failure cause:
+ |${failure.getMessage} ms
+ |execution: ${exec.toMillis.toString} ms""".stripMargin
+ )
+ case doobie.util.log.ProcessingFailure(sql, _, label, exec, processing, failure) =>
+ logger.error(failure)(
+ s"""SQL failed at processing:
+ |${label}
+ |${sql}
+ |failure cause:
+ |${failure.getMessage}
+ |execution: ${exec.toMillis.toString} ms
+ |processing: ${processing.toMillis.toString} ms
+ |total: ${(exec + processing).toMillis.toString} ms""".stripMargin
+ )
}
}
diff --git a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/KafkaEventBus.scala b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/KafkaEventBus.scala
index 1d5bd727..0f7974a1 100644
--- a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/KafkaEventBus.scala
+++ b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/KafkaEventBus.scala
@@ -1,35 +1,41 @@
package io.branchtalk.shared.infrastructure
-import cats.effect.Async
+import cats.Show
+import cats.data.NonEmptyList
+import cats.effect.{ Async, Concurrent, Sync, Temporal }
import com.typesafe.scalalogging.Logger
-import fs2.Stream
-import fs2.kafka._
-import io.branchtalk.shared.model.UUID
+import fs2.{ Pipe, Stream }
+import fs2.kafka.*
+import io.branchtalk.shared.model.{ ShowPretty, UUID }
+import io.branchtalk.shared.infrastructure.PureconfigSupport.{ *, given }
+import io.branchtalk.shared.model.AvroSerialization.DeserializationResult
+
+import scala.concurrent.duration.FiniteDuration
object KafkaEventBus {
+ type Producer[F[_], Event] = Pipe[F, (UUID, Event), ProducerResult[UUID, Event]]
+ type Consumer[F[_], Event] = Stream[F, CommittableConsumerRecord[F, UUID, Event]]
+ type Committer[F[_]] = Pipe[F, CommittableOffset[F], Unit]
+
private val logger = Logger(getClass)
- def producer[F[_]: Async, Event: Serializer[F, *]](
- settings: KafkaEventBusConfig
- ): EventBusProducer[F, Event] = (events: Stream[F, (UUID, Event)]) => {
- events
- .map { case (key, value) =>
- ProducerRecords.one(ProducerRecord(settings.topic.nonEmptyString.value, key, value))
- }
- .through(KafkaProducer.pipe(settings.toProducerConfig[F, Event]))
- .evalTap(e =>
- Async[F].delay(logger.info(show"${e.records.size} events published to ${settings.topic.nonEmptyString.value}"))
- )
- }
+ def producer[F[_]: Async, Event: Serializer[F, *]](busConfig: BusConfig): Producer[F, Event] =
+ (events: Stream[F, (UUID, Event)]) =>
+ events
+ .map { case (key, value) =>
+ ProducerRecords.one(ProducerRecord(busConfig.topic.unwrap, key, value))
+ }
+ .through(KafkaProducer.pipe(busConfig.toProducerConfig[F, Event]))
+ .evalTap(e => Async[F].delay(logger.info(show"${e.size} events published to ${busConfig.topic}")))
def consumer[F[_]: Async, Event: SafeDeserializer[F, *]](
- busConfig: KafkaEventBusConfig,
- consumerConfig: KafkaEventConsumerConfig
- ): EventBusConsumer[F, Event] =
+ busConfig: BusConfig,
+ consumerConfig: ConsumerConfig
+ ): Consumer[F, Event] =
KafkaConsumer
.stream(busConfig.toConsumerConfig[F, Event](consumerConfig))
- .evalTap(_.subscribeTo(busConfig.topic.nonEmptyString.value))
+ .evalTap(_.subscribeTo(busConfig.topic.unwrap))
.flatMap(_.stream)
.flatMap { commitable =>
commitable.record.value match {
@@ -48,4 +54,78 @@ object KafkaEventBus {
val CommittableConsumerRecord(record, offset) = commitable
CommittableConsumerRecord(ConsumerRecord(record.topic, record.partition, record.offset, record.key, value), offset)
}
+
+ type Topic = Topic.Type
+ object Topic extends Newtype[String] {
+
+ override inline def validate(input: String): Boolean = input.nonEmpty
+
+ def unapply(topic: Topic): Some[String] = Some(topic.unwrap)
+
+ given ConfigReader[Topic] = ConfigReader[String].emapString("Topic")(make)
+ given Show[Topic] = unsafeMakeF[Show](Show[String])
+ }
+
+ type ConsumerGroup = ConsumerGroup.Type
+ object ConsumerGroup extends Newtype[String] {
+
+ override inline def validate(input: String): Boolean = input.nonEmpty
+
+ def unapply(consumerGroup: ConsumerGroup): Some[String] = Some(consumerGroup.unwrap)
+
+ given ConfigReader[ConsumerGroup] = ConfigReader[String].emapString("ConsumerGroup")(make)
+ given Show[ConsumerGroup] = unsafeMakeF[Show](Show[String])
+ }
+
+ type MaxCommitSize = MaxCommitSize.Type
+ object MaxCommitSize extends Newtype[Int] {
+
+ override inline def validate(input: Int): Boolean = input > 0
+
+ def unapply(domainName: MaxCommitSize): Some[Int] = Some(domainName.unwrap)
+
+ given ConfigReader[MaxCommitSize] = ConfigReader[Int].emapString("MaxCommitSize")(make)
+ given Show[MaxCommitSize] = unsafeMakeF[Show](Show[Int])
+ }
+
+ type MaxCommitTime = MaxCommitTime.Type
+ object MaxCommitTime extends Newtype[FiniteDuration] {
+
+ def unapply(maxCommitTime: MaxCommitTime): Some[FiniteDuration] = Some(maxCommitTime.unwrap)
+
+ given ConfigReader[MaxCommitTime] = unsafeMakeF[ConfigReader](ConfigReader[FiniteDuration])
+ given Show[MaxCommitTime] = unsafeMakeF[Show](Show[FiniteDuration])
+ }
+
+ final case class BusConfig(
+ servers: NonEmptyList[Server],
+ topic: Topic,
+ cache: Server
+ ) derives ConfigReader,
+ ShowPretty {
+
+ def toConsumerConfig[F[_]: Sync, Event: SafeDeserializer[F, *]](
+ consumerConfig: ConsumerConfig
+ ): ConsumerSettings[F, UUID, DeserializationResult[Event]] =
+ ConsumerSettings(Deserializer.uuid[F], SafeDeserializer[F, Event])
+ .withAutoOffsetReset(AutoOffsetReset.Earliest)
+ .withBootstrapServers(servers.map(_.show).intercalate(","))
+ .withGroupId(consumerConfig.consumerGroup.unwrap)
+
+ def toProducerConfig[F[_]: Sync, Event: Serializer[F, *]]: ProducerSettings[F, UUID, Event] =
+ ProducerSettings(Serializer.uuid[F], Serializer[F, Event])
+ .withBootstrapServers(servers.map(_.show).intercalate(","))
+
+ def toCommitBatch[F[_]: Concurrent: Temporal](
+ consumerConfig: ConsumerConfig
+ ): Pipe[F, CommittableOffset[F], Unit] =
+ commitBatchWithin[F](consumerConfig.maxCommitSize.unwrap, consumerConfig.maxCommitTime.unwrap)
+ }
+
+ final case class ConsumerConfig(
+ consumerGroup: ConsumerGroup,
+ maxCommitSize: MaxCommitSize,
+ maxCommitTime: MaxCommitTime
+ ) derives ConfigReader,
+ ShowPretty
}
diff --git a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/KafkaEventBusConfig.scala b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/KafkaEventBusConfig.scala
deleted file mode 100644
index 661fb411..00000000
--- a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/KafkaEventBusConfig.scala
+++ /dev/null
@@ -1,34 +0,0 @@
-package io.branchtalk.shared.infrastructure
-
-import cats.data.NonEmptyList
-import cats.effect.{ Concurrent, Sync, Temporal }
-import fs2.kafka._
-import fs2.Pipe
-import io.branchtalk.shared.model.{ ShowPretty, UUID }
-import io.branchtalk.shared.infrastructure.PureconfigSupport._
-import io.branchtalk.shared.model.AvroSerialization.DeserializationResult
-import io.scalaland.catnip.Semi
-
-@Semi(ConfigReader, ShowPretty) final case class KafkaEventBusConfig(
- servers: NonEmptyList[Server],
- topic: Topic,
- cache: Server
-) {
-
- def toConsumerConfig[F[_]: Sync, Event: SafeDeserializer[F, *]](
- consumerConfig: KafkaEventConsumerConfig
- ): ConsumerSettings[F, UUID, DeserializationResult[Event]] =
- ConsumerSettings(Deserializer.uuid[F], SafeDeserializer[F, Event])
- .withAutoOffsetReset(AutoOffsetReset.Earliest)
- .withBootstrapServers(servers.map(_.show).intercalate(","))
- .withGroupId(consumerConfig.consumerGroup.nonEmptyString.value)
-
- def toProducerConfig[F[_]: Sync, Event: Serializer[F, *]]: ProducerSettings[F, UUID, Event] =
- ProducerSettings(Serializer.uuid[F], Serializer[F, Event])
- .withBootstrapServers(servers.map(_.show).intercalate(","))
-
- def toCommitBatch[F[_]: Concurrent: Temporal](
- consumerConfig: KafkaEventConsumerConfig
- ): Pipe[F, CommittableOffset[F], Unit] =
- commitBatchWithin[F](consumerConfig.maxCommitSize.positiveInt.value, consumerConfig.maxCommitTime.finiteDuration)
-}
diff --git a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/KafkaEventConsumerConfig.scala b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/KafkaEventConsumerConfig.scala
deleted file mode 100644
index 6df56806..00000000
--- a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/KafkaEventConsumerConfig.scala
+++ /dev/null
@@ -1,11 +0,0 @@
-package io.branchtalk.shared.infrastructure
-
-import io.branchtalk.shared.model.ShowPretty
-import io.scalaland.catnip.Semi
-import pureconfig.ConfigReader
-
-@Semi(ConfigReader, ShowPretty) final case class KafkaEventConsumerConfig(
- consumerGroup: ConsumerGroup,
- maxCommitSize: MaxCommitSize,
- maxCommitTime: MaxCommitTime
-)
diff --git a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/KafkaSerialization.scala b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/KafkaSerialization.scala
index 34db81d3..80998a33 100644
--- a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/KafkaSerialization.scala
+++ b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/KafkaSerialization.scala
@@ -8,9 +8,9 @@ import io.branchtalk.shared.model.AvroSerialization.DeserializationResult
object KafkaSerialization {
- implicit def kafkaSerializer[F[_]: Sync, A: Encoder]: Serializer[F, A] =
+ given [F[_]: Sync, A: Encoder: SchemaFor]: Serializer[F, A] =
Serializer.lift[F, A](AvroSerialization.serialize(_))
- implicit def kafkaDeserializer[F[_]: Sync, A: Decoder: SchemaFor]: SafeDeserializer[F, A] =
+ given [F[_]: Sync, A: Decoder: SchemaFor]: SafeDeserializer[F, A] =
Deserializer.lift[F, DeserializationResult[A]](AvroSerialization.deserialize(_))
}
diff --git a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/PostgresConfig.scala b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/PostgresConfig.scala
deleted file mode 100644
index d084fea1..00000000
--- a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/PostgresConfig.scala
+++ /dev/null
@@ -1,15 +0,0 @@
-package io.branchtalk.shared.infrastructure
-
-import io.scalaland.catnip.Semi
-import io.branchtalk.shared.infrastructure.PureconfigSupport._
-import io.branchtalk.shared.model.ShowPretty
-
-@Semi(ConfigReader, ShowPretty) final case class PostgresConfig(
- url: DatabaseURL,
- username: DatabaseUsername,
- password: DatabasePassword,
- schema: DatabaseSchema,
- domain: DatabaseDomain,
- connectionPool: DatabaseConnectionPool,
- migrationOnStart: DatabaseMigrationOnStart
-)
diff --git a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/PostgresDatabase.scala b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/PostgresDatabase.scala
index c408aab7..103a2a44 100644
--- a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/PostgresDatabase.scala
+++ b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/PostgresDatabase.scala
@@ -1,55 +1,57 @@
package io.branchtalk.shared.infrastructure
+import cats.Show
import cats.effect.{ Async, Resource, Sync }
import com.zaxxer.hikari.metrics.{ IMetricsTracker, MetricsTrackerFactory, PoolStats }
import com.zaxxer.hikari.metrics.prometheus.PrometheusHistogramMetricsTrackerFactory
-import doobie._
-import doobie.implicits._
+import doobie.*
+import doobie.implicits.*
import doobie.hikari.HikariTransactor
-import io.branchtalk.shared.infrastructure.PostgresDatabase.PrefixedMetricsTrackerFactory
+import io.branchtalk.logging.Logger
+import io.branchtalk.shared.infrastructure.DoobieSupport.{ *, given }
+import io.branchtalk.shared.infrastructure.PureconfigSupport.{ *, given }
+import io.branchtalk.shared.model.*
import io.prometheus.client.{ Collector, CollectorRegistry }
import org.flywaydb.core.Flyway
import scala.util.Random
-final class PostgresDatabase(config: PostgresConfig) {
+final class PostgresDatabase(config: PostgresDatabase.Config) {
private val randomPrefixLength = 6
private def flyway[F[_]: Sync] = Sync[F].delay(
Flyway
.configure()
- .dataSource(config.url.nonEmptyString.value,
- config.username.nonEmptyString.value,
- config.password.nonEmptyString.value
- )
- .schemas(config.schema.nonEmptyString.value)
- .table(s"flyway_${config.domain.nonEmptyString.value}_schema_history")
- .locations(s"db/${config.domain.nonEmptyString.value}/migrations")
+ .dataSource(config.url.unwrap, config.username.unwrap, config.password.unwrap)
+ .schemas(config.schema.unwrap)
+ .table(s"flyway_${config.domain.unwrap}_schema_history")
+ .locations(s"db/${config.domain.unwrap}/migrations")
.load()
)
- def transactor[F[_]: Async](registry: CollectorRegistry): Resource[F, HikariTransactor[F]] =
+ def transactor[F[_]: Async](logger: Logger[F], registry: CollectorRegistry): Resource[F, HikariTransactor[F]] =
for {
- connectEC <- doobie.util.ExecutionContexts.fixedThreadPool[F](config.connectionPool.positiveInt.value)
- xa <- HikariTransactor.initial[F](connectEC)
+ connectEC <- doobie.util.ExecutionContexts.fixedThreadPool[F](config.connectionPool.unwrap)
+ xa <- HikariTransactor.initial[F](connectEC, logHandler = Some(doobieLogger(logger)))
_ <- Resource.eval {
xa.configure { ds =>
Async[F].delay {
ds.setMetricsTrackerFactory(
- new PrefixedMetricsTrackerFactory(config.domain.nonEmptyString.value + "_" + LazyList
- .continually(Random.nextPrintableChar())
- .filter(_.isLetter)
- .take(randomPrefixLength)
- .mkString,
- registry
+ new PostgresDatabase.PrefixedMetricsTrackerFactory(
+ config.domain.unwrap + "_" + LazyList
+ .continually(Random.nextPrintableChar())
+ .filter(_.isLetter)
+ .take(randomPrefixLength)
+ .mkString,
+ registry
)
)
- ds.setJdbcUrl(config.url.nonEmptyString.value)
- ds.setUsername(config.username.nonEmptyString.value)
- ds.setPassword(config.password.nonEmptyString.value)
+ ds.setJdbcUrl(config.url.unwrap)
+ ds.setUsername(config.username.unwrap)
+ ds.setPassword(config.password.unwrap)
ds.setMaxLifetime(5 * 60 * 1000)
- ds.setSchema(config.schema.nonEmptyString.value)
+ ds.setSchema(config.schema.unwrap)
}
}
}
@@ -58,16 +60,104 @@ final class PostgresDatabase(config: PostgresConfig) {
def migrate[F[_]: Sync]: F[Unit] = flyway[F].map(_.migrate()).void
- def healthCheck[F[_]: Sync](xa: Transactor[F]): F[String] = sql"select now()".query[String].unique.transact(xa)
+ def healthCheck[F[_]: Sync](xa: Transactor[F]): F[String] =
+ sql"select now()".queryWithLabel[String]("DB Health Check").unique.transact(xa)
}
object PostgresDatabase {
+ type URL = URL.Type
+ object URL extends Newtype[String] {
+
+ override inline def validate(input: String): Boolean = input.nonEmpty
+
+ def unapply(url: URL): Some[String] = Some(url.unwrap)
+
+ given ConfigReader[URL] = ConfigReader[String].emapString("URL")(make)
+ given Show[URL] = unsafeMakeF[Show](Show[String])
+ }
+
+ type Username = Username.Type
+ object Username extends Newtype[String] {
+
+ override inline def validate(input: String): Boolean = input.nonEmpty
+
+ def unapply(username: Username): Some[String] = Some(username.unwrap)
+
+ given ConfigReader[Username] = ConfigReader[String].emapString("Username")(make)
+ given Show[Username] = unsafeMakeF[Show](Show[String])
+ }
+
+ // not a newtype to override toString
+ final case class Password private (unwrap: String) {
+ override def toString: String = "[PASSWORD]"
+ }
+ object Password {
+ def parse(string: String): Either[String, Password] =
+ if string.nonEmpty then Right(Password(string)) else Left("Password cannot be empty")
+
+ given ConfigReader[Password] = ConfigReader[String].emapString("Password")(parse)
+ given Show[Password] = _ => "PASSWORD"
+ }
+
+ type Schema = Schema.Type
+ object Schema extends Newtype[String] {
+
+ override inline def validate(input: String): Boolean = input.nonEmpty
+
+ def unapply(domainName: Schema): Some[String] = Some(domainName.unwrap)
+
+ given ConfigReader[Schema] = ConfigReader[String].emapString("Schema")(make)
+ given Show[Schema] = unsafeMakeF[Show](Show[String])
+ }
+
+ type Domain = Domain.Type
+ object Domain extends Newtype[String] {
+
+ override inline def validate(input: String): Boolean = input.nonEmpty
+
+ def unapply(domain: Domain): Some[String] = Some(domain.unwrap)
+
+ given ConfigReader[Domain] = ConfigReader[String].emapString("Domain")(make)
+ given Show[Domain] = unsafeMakeF[Show](Show[String])
+ }
+
+ type ConnectionPool = ConnectionPool.Type
+ object ConnectionPool extends Newtype[Int] {
+
+ override inline def validate(input: Int): Boolean = input > 0
+
+ def unapply(connectionPool: ConnectionPool): Some[Int] = Some(connectionPool.unwrap)
+
+ given ConfigReader[ConnectionPool] = ConfigReader[Int].emapString("ConnectionPool")(make)
+ given Show[ConnectionPool] = unsafeMakeF[Show](Show[Int])
+ }
+
+ type MigrationOnStart = MigrationOnStart.Type
+ object MigrationOnStart extends Newtype[Boolean] {
+
+ def unapply(migrationOnStart: MigrationOnStart): Some[Boolean] = Some(migrationOnStart.unwrap)
+
+ given ConfigReader[MigrationOnStart] = unsafeMakeF[ConfigReader](ConfigReader[Boolean])
+ given Show[MigrationOnStart] = unsafeMakeF[Show](Show[Boolean])
+ }
+
+ final case class Config(
+ url: URL,
+ username: Username,
+ password: Password,
+ schema: Schema,
+ domain: Domain,
+ connectionPool: ConnectionPool,
+ migrationOnStart: MigrationOnStart
+ ) derives ConfigReader,
+ ShowPretty
+
// suppress "Collector already registered that provides name: hikaricp_"
- final class NonComplainingCollectorRegistry(impl: CollectorRegistry) extends CollectorRegistry {
+ final private class NonComplainingCollectorRegistry(impl: CollectorRegistry) extends CollectorRegistry {
override def register(m: Collector): Unit = try impl.register(m)
catch { case _: IllegalArgumentException => /* suppressed */ }
override def unregister(m: Collector): Unit = impl.unregister(m)
- override def clear(): Unit = impl.clear()
+ override def clear(): Unit = impl.clear()
override def metricFamilySamples(): java.util.Enumeration[Collector.MetricFamilySamples] =
impl.metricFamilySamples()
override def filteredMetricFamilySamples(
diff --git a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/Projector.scala b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/Projector.scala
index 07f99015..87d831f8 100644
--- a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/Projector.scala
+++ b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/Projector.scala
@@ -1,9 +1,9 @@
package io.branchtalk.shared.infrastructure
import cats.Monoid
-import fs2._
-import _root_.io.branchtalk.logging.{ CorrelationID, MDC }
import cats.effect.Sync
+import fs2.{ io => _, * }
+import io.branchtalk.logging.{ CorrelationID, MDC }
trait Projector[F[_], -I, +O] extends Pipe[F, I, O] {
def contramap[I2](f: I2 => I): Projector[F, I2, O] = (_: Stream[F, I2]).map(f).through(apply)
@@ -14,14 +14,14 @@ trait Projector[F[_], -I, +O] extends Pipe[F, I, O] {
def mapStream[O2](f: O => Stream[F, O2]): Projector[F, I, O2] = (_: Stream[F, I]).through(apply).flatMap(f)
- def withCorrelationID[A](correlationID: CorrelationID)(fa: F[A])(implicit F: Sync[F], mdc: MDC[F]): F[A] =
+ def withCorrelationID[A](correlationID: CorrelationID)(fa: F[A])(using Sync[F], MDC[F]): F[A] =
correlationID.updateMDC[F] >> fa
}
object Projector {
def lift[F[_], I, O](pipe: Pipe[F, I, O]): Projector[F, I, O] = (i: Stream[F, I]) => pipe(i)
- implicit def monoid[F[_], I, O]: Monoid[Projector[F, I, O]] = new Monoid[Projector[F, I, O]] {
+ given [F[_], I, O]: Monoid[Projector[F, I, O]] = new {
override def empty: Projector[F, I, O] = lift(Monoid[Pipe[F, I, O]].empty)
override def combine(x: Projector[F, I, O], y: Projector[F, I, O]): Projector[F, I, O] =
lift(Monoid[Pipe[F, I, O]].combine(x, y))
diff --git a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/PureconfigSupport.scala b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/PureconfigSupport.scala
index b437c445..aabb60b8 100644
--- a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/PureconfigSupport.scala
+++ b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/PureconfigSupport.scala
@@ -1,100 +1,45 @@
package io.branchtalk.shared.infrastructure
-import cats.{ Alternative, Foldable }
-import cats.data.{ Chain, NonEmptyChain, NonEmptyList, NonEmptyMap, NonEmptySet, NonEmptyVector }
-import cats.kernel.Order
-import enumeratum._
-import enumeratum.values._
-import eu.timepit.refined.api._
-
-import scala.collection.immutable.SortedSet
-import scala.reflect.ClassTag
-import scala.reflect.runtime.universe.WeakTypeTag
-
// Allows `import PureconfigSupport._` instead of `import pureconfig._, pureconfig.module.cats._, ...`.
-// `pureconfig` modules are objects, there are no traits so if we want to have everything in one place we have to...
object PureconfigSupport extends LowPriorityPureconfigImplicit {
- type ConfigReader[A] = pureconfig.ConfigReader[A]
- val ConfigReader = pureconfig.ConfigReader
+ export pureconfig.{ ConfigReader, ConfigSource, ConfigWriter }
+ export pureconfig.generic.derivation.default.derived
- type ConfigWriter[A] = pureconfig.ConfigWriter[A]
- val ConfigWriter = pureconfig.ConfigWriter
-
- type ConfigSource = pureconfig.ConfigSource
- val ConfigSource = pureconfig.ConfigSource
+ extension [A](reader: ConfigReader[A]) {
+ @SuppressWarnings(Array("org.wartremover.warts.ToString"))
+ def emapString[B](tpe: String)(f: A => Either[String, B]): ConfigReader[B] =
+ reader.emap(a => f(a).left.map(err => pureconfig.error.CannotConvert(a.toString, tpe, err)))
+ }
// Cats
- implicit def nonEmptyListReader[A](implicit reader: ConfigReader[List[A]]): ConfigReader[NonEmptyList[A]] =
- pureconfig.module.cats.nonEmptyListReader[A]
- implicit def nonEmptyListWriter[A](implicit writer: ConfigWriter[List[A]]): ConfigWriter[NonEmptyList[A]] =
- pureconfig.module.cats.nonEmptyListWriter[A]
-
- implicit def nonEmptyVectorReader[A](implicit reader: ConfigReader[Vector[A]]): ConfigReader[NonEmptyVector[A]] =
- pureconfig.module.cats.nonEmptyVectorReader[A]
- implicit def nonEmptyVectorWriter[A](implicit writer: ConfigWriter[Vector[A]]): ConfigWriter[NonEmptyVector[A]] =
- pureconfig.module.cats.nonEmptyVectorWriter[A]
-
- implicit def nonEmptySetReader[A](implicit reader: ConfigReader[SortedSet[A]]): ConfigReader[NonEmptySet[A]] =
- pureconfig.module.cats.nonEmptySetReader[A]
- implicit def nonEmptySetWriter[A](implicit writer: ConfigWriter[SortedSet[A]]): ConfigWriter[NonEmptySet[A]] =
- pureconfig.module.cats.nonEmptySetWriter[A]
-
- implicit def nonEmptyMapReader[A, B](implicit
- reader: ConfigReader[Map[A, B]],
- ord: Order[A]
- ): ConfigReader[NonEmptyMap[A, B]] =
- pureconfig.module.cats.nonEmptyMapReader[A, B]
- implicit def nonEmptyMapWriter[A, B](implicit writer: ConfigWriter[Map[A, B]]): ConfigWriter[NonEmptyMap[A, B]] =
- pureconfig.module.cats.nonEmptyMapWriter[A, B]
-
- // For emptiable foldables not covered by TraversableOnce reader/writer, e.g. Chain.
- implicit def lowPriorityNonReducibleReader[A, F[_]: Foldable: Alternative](implicit
- reader: ConfigReader[List[A]]
- ): pureconfig.Exported[ConfigReader[F[A]]] =
- pureconfig.module.cats.lowPriorityNonReducibleReader[A, F]
- implicit def lowPriorityNonReducibleWriter[A, F[_]: Foldable: Alternative](implicit
- writer: ConfigWriter[List[A]]
- ): pureconfig.Exported[ConfigWriter[F[A]]] =
- pureconfig.module.cats.lowPriorityNonReducibleWriter[A, F]
-
- implicit def nonEmptyChainReader[A](implicit reader: ConfigReader[Chain[A]]): ConfigReader[NonEmptyChain[A]] =
- pureconfig.module.cats.nonEmptyChainReader[A]
- implicit def nonEmptyChainWriter[A](implicit writer: ConfigWriter[Chain[A]]): ConfigWriter[NonEmptyChain[A]] =
- pureconfig.module.cats.nonEmptyChainWriter[A]
+ export pureconfig.module.cats.{
+ lowPriorityNonReducibleReader,
+ lowPriorityNonReducibleWriter,
+ nonEmptyChainReader,
+ nonEmptyChainWriter,
+ nonEmptyListReader,
+ nonEmptyListWriter,
+ nonEmptyMapReader,
+ nonEmptyMapWriter,
+ nonEmptySetReader,
+ nonEmptySetWriter,
+ nonEmptyVectorReader,
+ nonEmptyVectorWriter
+ }
// enumeratum
- implicit def enumeratumConfigConvert[A <: EnumEntry: Enum: ClassTag]: pureconfig.ConfigConvert[A] =
- pureconfig.module.enumeratum.enumeratumConfigConvert[A]
-
- implicit def enumeratumIntConfigConvert[A <: IntEnumEntry: IntEnum: ClassTag]: pureconfig.ConfigConvert[A] =
- pureconfig.module.enumeratum.enumeratumIntConfigConvert[A]
-
- implicit def enumeratumLongConfigConvert[A <: LongEnumEntry: LongEnum: ClassTag]: pureconfig.ConfigConvert[A] =
- pureconfig.module.enumeratum.enumeratumLongConfigConvert[A]
-
- implicit def enumeratumShortConfigConvert[A <: ShortEnumEntry: ShortEnum: ClassTag]: pureconfig.ConfigConvert[A] =
- pureconfig.module.enumeratum.enumeratumShortConfigConvert[A]
-
- implicit def enumeratumStringConfigConvert[A <: StringEnumEntry: StringEnum: ClassTag]: pureconfig.ConfigConvert[A] =
- pureconfig.module.enumeratum.enumeratumStringConfigConvert[A]
-
- implicit def enumeratumByteConfigConvert[A <: ByteEnumEntry: ByteEnum: ClassTag]: pureconfig.ConfigConvert[A] =
- pureconfig.module.enumeratum.enumeratumByteConfigConvert[A]
-
- implicit def enumeratumCharConfigConvert[A <: CharEnumEntry: CharEnum: ClassTag]: pureconfig.ConfigConvert[A] =
- pureconfig.module.enumeratum.enumeratumCharConfigConvert[A]
-
- // refined
-
- implicit def refTypeConfigConvert[F[_, _], T, P](implicit
- configConvert: pureconfig.ConfigConvert[T],
- refType: RefType[F],
- validate: Validate[T, P],
- typeTag: WeakTypeTag[F[T, P]]
- ): pureconfig.ConfigConvert[F[T, P]] = eu.timepit.refined.pureconfig.refTypeConfigConvert[F, T, P]
+ export pureconfig.module.enumeratum.{
+ enumeratumByteConfigConvert,
+ enumeratumCharConfigConvert,
+ enumeratumConfigConvert,
+ enumeratumIntConfigConvert,
+ enumeratumLongConfigConvert,
+ enumeratumShortConfigConvert,
+ enumeratumStringConfigConvert
+ }
}
// for some reason original traversableReader is not seen, maybe because author forgot to annotate it?
diff --git a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/Reads.scala b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/Reads.scala
index 5dac51a8..92bd6ed6 100644
--- a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/Reads.scala
+++ b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/Reads.scala
@@ -1,8 +1,8 @@
package io.branchtalk.shared.infrastructure
import cats.effect.Sync
-import doobie._
-import doobie.implicits._
+import doobie.*
+import doobie.implicits.*
import fs2.Stream
// Utilities for reads services.
@@ -16,3 +16,10 @@ abstract class Reads[F[_]: Sync, Entity](transactor: Transactor[F]) {
final protected def queryStream(query: Query0[Entity]): Stream[F, Entity] = query.stream.transact(transactor)
}
+object Reads {
+
+ final case class Infrastructure[F[_], Event](
+ transactor: Transactor[F],
+ consumer: KafkaEventBus.ConsumerConfig => ConsumerStream[F, Event]
+ )
+}
diff --git a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/SafeDeserializer.scala b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/SafeDeserializer.scala
new file mode 100644
index 00000000..b892a9bd
--- /dev/null
+++ b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/SafeDeserializer.scala
@@ -0,0 +1,10 @@
+package io.branchtalk.shared.infrastructure
+
+import fs2.kafka.Deserializer
+import io.branchtalk.shared.model.AvroSerialization.DeserializationResult
+
+type SafeDeserializer[F[_], Event] = Deserializer[F, DeserializationResult[Event]]
+object SafeDeserializer {
+
+ inline def apply[F[_], Event](using sd: SafeDeserializer[F, Event]): SafeDeserializer[F, Event] = sd
+}
diff --git a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/Server.scala b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/Server.scala
index a6d0d928..e7c87431 100644
--- a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/Server.scala
+++ b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/Server.scala
@@ -1,16 +1,35 @@
package io.branchtalk.shared.infrastructure
import cats.Show
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.numeric.Positive
-import eu.timepit.refined.types.string.NonEmptyString
-import io.branchtalk.shared.infrastructure.PureconfigSupport._
-import io.scalaland.catnip.Semi
-
-@Semi(ConfigReader) final case class Server(
- host: NonEmptyString,
- port: Int Refined Positive
-)
+import io.branchtalk.shared.infrastructure.PureconfigSupport.{ *, given }
+
+final case class Server(
+ host: Server.Host,
+ port: Server.Port
+) derives ConfigReader
object Server {
- implicit def show: Show[Server] = (s: Server) => show"${s.host.value}:${s.port.value}"
+
+ type Host = Host.Type
+ object Host extends Newtype[String] {
+
+ override inline def validate(input: String): Boolean = input.nonEmpty
+
+ def unapply(host: Host): Some[String] = Some(host.unwrap)
+
+ given ConfigReader[Host] = ConfigReader[String].emapString("Host")(make)
+ given Show[Host] = unsafeMakeF[Show](Show[String])
+ }
+
+ type Port = Port.Type
+ object Port extends Newtype[Int] {
+
+ override inline def validate(input: Int): Boolean = input > 0
+
+ def unapply(port: Port): Some[Int] = Some(port.unwrap)
+
+ given ConfigReader[Port] = ConfigReader[Int].emapString("Port")(make)
+ given Show[Port] = unsafeMakeF[Show](Show[Int])
+ }
+
+ given Show[Server] = (s: Server) => show"${s.host}:${s.port}"
}
diff --git a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/StreamRunner.scala b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/StreamRunner.scala
index da8eea17..ac777b09 100644
--- a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/StreamRunner.scala
+++ b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/StreamRunner.scala
@@ -2,9 +2,9 @@ package io.branchtalk.shared.infrastructure
import cats.{ Monad, Semigroup }
import cats.effect.{ Async, Deferred, Resource }
-import cats.effect.implicits._
+import cats.effect.implicits.*
import fs2.Stream
-import io.branchtalk.shared.model.Logger
+import io.branchtalk.logging.Logger
// Start Stream as a Fiber, close it gracefully when releasing the resource.
final case class StreamRunner[F[_]](asResource: Resource[F, Unit])
@@ -30,13 +30,12 @@ object StreamRunner {
)
}
.void
- .handleErrorWith { error: Throwable =>
+ .handleErrorWith { (error: Throwable) =>
Resource.eval(
logger.error(error)("Error occurred before kill-switch was triggered") >> error.raiseError[F, Unit]
)
}
}
- implicit def semigroup[F[_]: Monad]: Semigroup[StreamRunner[F]] =
- (a, b) => StreamRunner(a.asResource >> b.asResource)
+ given [F[_]: Monad]: Semigroup[StreamRunner[F]] = (a, b) => StreamRunner(a.asResource >> b.asResource)
}
diff --git a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/Writes.scala b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/Writes.scala
index f6788d5d..1e125f50 100644
--- a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/Writes.scala
+++ b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/Writes.scala
@@ -1,39 +1,52 @@
package io.branchtalk.shared.infrastructure
import cats.effect.Sync
-import io.branchtalk.shared.infrastructure.DoobieSupport._
+import io.branchtalk.shared.infrastructure.DoobieSupport.{ *, given }
import io.branchtalk.shared.model.{ CodePosition, CommonError, ID, UUID }
-import fs2._
+import fs2.*
// Utilities for writes services.
-abstract class Writes[F[_]: Sync, Entity, Event](producer: EventBusProducer[F, Event]) {
-
- implicit private val logHandler: LogHandler = doobieLogger(getClass)
+abstract class Writes[F[_]: Sync, Entity, Event](producer: KafkaEventBus.Producer[F, Event]) {
// sending event to internal bus as a part of a write model
final protected def postEvent(id: ID[Entity], event: Event): F[Unit] =
- producer(Stream[F, (UUID, Event)](id.uuid -> event)).compile.drain
+ producer(Stream[F, (UUID, Event)](id.unwrap -> event)).compile.drain
protected class EntityCheck(entity: String, transactor: Transactor[F]) {
- def apply(entityID: ID[Entity], fragment: Fragment)(implicit codePosition: CodePosition): F[Unit] =
- fragment.exists.transact(transactor).flatMap {
+ def apply(entityID: ID[Entity], fragment: Fragment)(using CodePosition): F[Unit] =
+ fragment.exists(show"Check that $entity ID=$entityID exists").transact(transactor).flatMap {
case true => Sync[F].unit
- case false => (CommonError.NotFound(entity, entityID, codePosition): Throwable).raiseError[F, Unit]
+ case false => (CommonError.notFound(entity, entityID): Throwable).raiseError[F, Unit]
}
}
protected class ParentCheck[Parent](entity: String, transactor: Transactor[F]) {
- def apply(parentID: ID[Parent], fragment: Fragment)(implicit codePosition: CodePosition): F[Unit] =
- fragment.exists.transact(transactor).flatMap {
+ def apply(parentID: ID[Parent], fragment: Fragment)(using CodePosition): F[Unit] =
+ fragment.exists(show"Check that parental $entity ID=$parentID exists").transact(transactor).flatMap {
case true => Sync[F].unit
- case false => (CommonError.ParentNotExist(entity, parentID, codePosition): Throwable).raiseError[F, Unit]
+ case false => (CommonError.parentNotExist(entity, parentID): Throwable).raiseError[F, Unit]
}
- def withValue[T: Meta](parentID: ID[Parent], fragment: Fragment)(implicit codePosition: CodePosition): F[T] =
- fragment.query[T].option.transact(transactor).flatMap {
- case Some(t) => Sync[F].pure(t)
- case None => (CommonError.ParentNotExist(entity, parentID, codePosition): Throwable).raiseError[F, T]
- }
+ def withValue[T: Meta](parentID: ID[Parent], fragment: Fragment)(using CodePosition): F[T] =
+ fragment
+ .queryWithLabel[T](show"Check that parental $entity ID=$parentID exists")
+ .option
+ .transact(transactor)
+ .flatMap {
+ case Some(t) => Sync[F].pure(t)
+ case None => (CommonError.parentNotExist(entity, parentID): Throwable).raiseError[F, T]
+ }
}
}
+object Writes {
+
+ final case class Infrastructure[F[_], Event, InternalEvent](
+ transactor: Transactor[F],
+ internalProducer: KafkaEventBus.Producer[F, InternalEvent],
+ internalConsumerStream: ConsumerStream[F, InternalEvent],
+ producer: KafkaEventBus.Producer[F, Event],
+ consumerStream: ConsumerStream.Factory[F, Event],
+ cache: Cache[F, String, Event]
+ )
+}
diff --git a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/filters.scala b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/filters.scala
new file mode 100644
index 00000000..1edcbd8d
--- /dev/null
+++ b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/filters.scala
@@ -0,0 +1,11 @@
+package io.branchtalk.shared.infrastructure
+
+import cats.MonadThrow
+
+extension [F[_]: MonadThrow, A](fa: F[A]) {
+ // suppress complains from for-comprehension and case
+ def withFilter(f: A => Boolean) = fa.flatMap { a =>
+ if (f(a)) a.pure[F]
+ else (new NoSuchElementException: Throwable).raiseError[F, A]
+ }
+}
diff --git a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/package.scala b/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/package.scala
deleted file mode 100644
index a8d24adb..00000000
--- a/modules/common-infrastructure/src/main/scala/io/branchtalk/shared/infrastructure/package.scala
+++ /dev/null
@@ -1,107 +0,0 @@
-package io.branchtalk.shared
-
-import cats.Show
-import eu.timepit.refined.numeric.Positive
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.types.string.NonEmptyString
-import fs2.{ Pipe, Stream }
-import fs2.kafka.{ CommittableConsumerRecord, CommittableOffset, Deserializer, ProducerResult }
-import io.branchtalk.shared.infrastructure.PureconfigSupport._
-import io.branchtalk.shared.model.AvroSerialization.DeserializationResult
-import io.branchtalk.shared.model._
-import io.estatico.newtype.macros.newtype
-import io.estatico.newtype.ops._
-
-import scala.concurrent.duration.FiniteDuration
-
-package object infrastructure {
-
- type EventBusProducer[F[_], Event] = Pipe[F, (UUID, Event), ProducerResult[Unit, UUID, Event]]
- type EventBusConsumer[F[_], Event] = Stream[F, CommittableConsumerRecord[F, UUID, Event]]
- type EventBusCommitter[F[_]] = Pipe[F, CommittableOffset[F], Unit]
-
- type SafeDeserializer[F[_], Event] = Deserializer[F, DeserializationResult[Event]]
- object SafeDeserializer {
- @inline def apply[F[_], Event](implicit sd: SafeDeserializer[F, Event]): SafeDeserializer[F, Event] = sd
- }
-
- @newtype final case class DomainName(nonEmptyString: NonEmptyString)
- object DomainName {
- def unapply(domainName: DomainName): Some[NonEmptyString] = Some(domainName.nonEmptyString)
-
- implicit val configReader: ConfigReader[DomainName] = ConfigReader[NonEmptyString].coerce
- implicit val show: Show[DomainName] = Show.wrap(_.nonEmptyString.value)
- }
-
- @newtype final case class DatabaseURL(nonEmptyString: NonEmptyString)
- object DatabaseURL {
- def unapply(databaseURL: DatabaseURL): Some[NonEmptyString] = Some(databaseURL.nonEmptyString)
-
- implicit val configReader: ConfigReader[DatabaseURL] = ConfigReader[NonEmptyString].coerce
- implicit val show: Show[DatabaseURL] = Show.wrap(_.nonEmptyString.value)
- }
- @newtype final case class DatabaseUsername(nonEmptyString: NonEmptyString)
- object DatabaseUsername {
- def unapply(databaseUsername: DatabaseUsername): Some[NonEmptyString] = Some(databaseUsername.nonEmptyString)
-
- implicit val configReader: ConfigReader[DatabaseUsername] = ConfigReader[NonEmptyString].coerce
- implicit val show: Show[DatabaseUsername] = Show.wrap(_.nonEmptyString.value)
- }
- @newtype final case class DatabaseSchema(nonEmptyString: NonEmptyString)
- object DatabaseSchema {
- def unapply(databaseSchema: DatabaseSchema): Some[NonEmptyString] = Some(databaseSchema.nonEmptyString)
-
- implicit val configReader: ConfigReader[DatabaseSchema] = ConfigReader[NonEmptyString].coerce
- implicit val show: Show[DatabaseSchema] = Show.wrap(_.nonEmptyString.value)
- }
- @newtype final case class DatabaseDomain(nonEmptyString: NonEmptyString)
- object DatabaseDomain {
- def unapply(databaseDomain: DatabaseDomain): Some[NonEmptyString] = Some(databaseDomain.nonEmptyString)
-
- implicit val configReader: ConfigReader[DatabaseDomain] = ConfigReader[NonEmptyString].coerce
- implicit val show: Show[DatabaseDomain] = Show.wrap(_.nonEmptyString.value)
- }
- @newtype final case class DatabaseConnectionPool(positiveInt: Int Refined Positive)
- object DatabaseConnectionPool {
- def unapply(connectionPool: DatabaseConnectionPool): Some[Int Refined Positive] = Some(connectionPool.positiveInt)
-
- implicit val configReader: ConfigReader[DatabaseConnectionPool] = ConfigReader[Int Refined Positive].coerce
- implicit val show: Show[DatabaseConnectionPool] = Show.wrap(_.positiveInt.value)
- }
- @newtype final case class DatabaseMigrationOnStart(bool: Boolean)
- object DatabaseMigrationOnStart {
- def unapply(migrationOnStart: DatabaseMigrationOnStart): Some[Boolean] = Some(migrationOnStart.bool)
-
- implicit val configReader: ConfigReader[DatabaseMigrationOnStart] = ConfigReader[Boolean].coerce
- implicit val show: Show[DatabaseMigrationOnStart] = Show.wrap(_.bool)
- }
-
- @newtype final case class Topic(nonEmptyString: NonEmptyString)
- object Topic {
- def unapply(topic: Topic): Some[NonEmptyString] = Some(topic.nonEmptyString)
-
- implicit val configReader: ConfigReader[Topic] = ConfigReader[NonEmptyString].coerce
- implicit val show: Show[Topic] = Show.wrap(_.nonEmptyString.value)
- }
- @newtype final case class ConsumerGroup(nonEmptyString: NonEmptyString)
- object ConsumerGroup {
- def unapply(consumerGroup: ConsumerGroup): Some[NonEmptyString] = Some(consumerGroup.nonEmptyString)
-
- implicit val configReader: ConfigReader[ConsumerGroup] = ConfigReader[NonEmptyString].coerce
- implicit val show: Show[ConsumerGroup] = Show.wrap(_.nonEmptyString.value)
- }
- @newtype final case class MaxCommitSize(positiveInt: Int Refined Positive)
- object MaxCommitSize {
- def unapply(maxCommitSize: MaxCommitSize): Some[Int Refined Positive] = Some(maxCommitSize.positiveInt)
-
- implicit val configReader: ConfigReader[MaxCommitSize] = ConfigReader[Int Refined Positive].coerce
- implicit val show: Show[MaxCommitSize] = Show.wrap(_.positiveInt.value)
- }
- @newtype final case class MaxCommitTime(finiteDuration: FiniteDuration)
- object MaxCommitTime {
- def unapply(maxCommitTime: MaxCommitTime): Some[FiniteDuration] = Some(maxCommitTime.finiteDuration)
-
- implicit val configReader: ConfigReader[MaxCommitTime] = ConfigReader[FiniteDuration].coerce
- implicit val show: Show[MaxCommitTime] = Show.wrap(_.finiteDuration)
- }
-}
diff --git a/modules/common-infrastructure/src/it/resources/logback-test.xml b/modules/common-infrastructure/src/test/resources/logback-test.xml
similarity index 100%
rename from modules/common-infrastructure/src/it/resources/logback-test.xml
rename to modules/common-infrastructure/src/test/resources/logback-test.xml
diff --git a/modules/common-infrastructure/src/it/scala/io/branchtalk/IOTest.scala b/modules/common-infrastructure/src/test/scala/io/branchtalk/IOTest.scala
similarity index 70%
rename from modules/common-infrastructure/src/it/scala/io/branchtalk/IOTest.scala
rename to modules/common-infrastructure/src/test/scala/io/branchtalk/IOTest.scala
index 25d282f9..4fed771d 100644
--- a/modules/common-infrastructure/src/it/scala/io/branchtalk/IOTest.scala
+++ b/modules/common-infrastructure/src/test/scala/io/branchtalk/IOTest.scala
@@ -5,19 +5,18 @@ import cats.effect.unsafe.implicits.global
import io.branchtalk.logging.MDC
import io.branchtalk.logging.MDC.Ctx
import io.branchtalk.shared.model.CodePosition
-import org.specs2.matcher.MatchResult
+import org.specs2.execute.Result
import org.specs2.specification.core.{ AsExecution, Execution }
-import org.specs2.matcher.Matchers._
-import org.specs2.matcher.MustMatchers.theValue
+import org.specs2.matcher.MustMatchers.{ *, given }
-import scala.concurrent.duration._
+import scala.concurrent.duration.*
trait IOTest {
- val pass: MatchResult[Boolean] = true must beTrue
+ val pass: Result = true must beTrue
// we don't rely on MDC in tests
- implicit protected val noopMDC: MDC[IO] = new MDC[IO] {
+ protected given noopMDC: MDC[IO] = new MDC[IO] {
override def ctx: IO[Ctx] = IO.pure(Map.empty)
override def get(key: String): IO[Option[String]] = IO.pure(None)
@@ -25,10 +24,10 @@ trait IOTest {
override def set(key: String, value: String): IO[Unit] = IO.unit
}
- implicit class IOTestOps[T](private val io: IO[T]) {
+ extension [T](io: IO[T]) {
- def eventually(retry: Int = 50, delay: FiniteDuration = 250.millis, timeout: FiniteDuration = 15.seconds)(implicit
- codePosition: CodePosition
+ def eventually(retry: Int = 50, delay: FiniteDuration = 250.millis, timeout: FiniteDuration = 15.seconds)(using
+ codePosition: CodePosition
): IO[T] = {
def withRetry(attemptsLeft: Int): PartialFunction[Throwable, IO[T]] = { case cause: Throwable =>
if (attemptsLeft <= 0)
@@ -43,7 +42,7 @@ trait IOTest {
io.flatTap(current => IO(scala.Predef.assert(condition(current), msg)))
}
- implicit protected def ioAsTest[T: AsExecution]: AsExecution[IO[T]] = new AsExecution[IO[T]] {
+ protected given ioAsTest[T: AsExecution]: AsExecution[IO[T]] = new AsExecution[IO[T]] {
override def execute(t: => IO[T]): Execution = AsExecution[T].execute(t.unsafeRunSync())
}
}
diff --git a/modules/common-infrastructure/src/it/scala/io/branchtalk/ResourcefulTest.scala b/modules/common-infrastructure/src/test/scala/io/branchtalk/ResourcefulTest.scala
similarity index 52%
rename from modules/common-infrastructure/src/it/scala/io/branchtalk/ResourcefulTest.scala
rename to modules/common-infrastructure/src/test/scala/io/branchtalk/ResourcefulTest.scala
index a4e42e51..afe0b270 100644
--- a/modules/common-infrastructure/src/it/scala/io/branchtalk/ResourcefulTest.scala
+++ b/modules/common-infrastructure/src/test/scala/io/branchtalk/ResourcefulTest.scala
@@ -4,9 +4,9 @@ import cats.effect.std.Dispatcher
import cats.effect.{ IO, Resource }
import cats.effect.unsafe.implicits.global
import io.prometheus.client.CollectorRegistry
-import org.specs2.specification.BeforeAfterAll
+import org.specs2.specification.core.Execution
-trait ResourcefulTest extends BeforeAfterAll {
+trait ResourcefulTest extends org.specs2.specification.Resource[Unit] {
// populated by resources
protected var registry: CollectorRegistry = _
@@ -14,10 +14,11 @@ trait ResourcefulTest extends BeforeAfterAll {
protected def testResource: Resource[IO, Unit] = {
Resource.make(IO(new CollectorRegistry().tap(registry = _)))(cr => IO(cr.clear())) >>
- Dispatcher[IO].map(dispatcher = _)
+ Dispatcher.parallel[IO].map(dispatcher = _)
}.void
- private var release: IO[Unit] = IO.unit
- override def beforeAll(): Unit = release = testResource.allocated.unsafeRunSync()._2
- override def afterAll(): Unit = release.unsafeRunSync()
+ private var release: IO[Unit] = IO.unit
+ protected def acquire: scala.concurrent.Future[Unit] =
+ testResource.allocated.map((_, releaseIO) => release = releaseIO).unsafeToFuture()
+ protected def release(resource: Unit): Execution = release.unsafeRunSync()
}
diff --git a/modules/common-infrastructure/src/test/scala/io/branchtalk/shared/infrastructure/TestKafkaEventBusConfig.scala b/modules/common-infrastructure/src/test/scala/io/branchtalk/shared/infrastructure/TestKafkaEventBusConfig.scala
new file mode 100644
index 00000000..c506b711
--- /dev/null
+++ b/modules/common-infrastructure/src/test/scala/io/branchtalk/shared/infrastructure/TestKafkaEventBusConfig.scala
@@ -0,0 +1,18 @@
+package io.branchtalk.shared.infrastructure
+
+import cats.data.NonEmptyList
+import io.branchtalk.shared.infrastructure.PureconfigSupport.{ *, given }
+import io.scalaland.chimney.dsl.*
+
+final case class TestKafkaEventBusConfig(
+ servers: NonEmptyList[Server],
+ topicPrefix: KafkaEventBus.Topic,
+ cache: Server
+) derives ConfigReader {
+
+ def topic(generatedSuffix: String): KafkaEventBus.Topic =
+ KafkaEventBus.Topic.unsafeMake(topicPrefix.unwrap + generatedSuffix)
+
+ def toKafkaEventBusConfig(generatedSuffix: String): KafkaEventBus.BusConfig =
+ this.into[KafkaEventBus.BusConfig].withFieldConst(_.topic, topic(generatedSuffix)).transform
+}
diff --git a/modules/common-infrastructure/src/it/scala/io/branchtalk/shared/infrastructure/TestKafkaResources.scala b/modules/common-infrastructure/src/test/scala/io/branchtalk/shared/infrastructure/TestKafkaResources.scala
similarity index 63%
rename from modules/common-infrastructure/src/it/scala/io/branchtalk/shared/infrastructure/TestKafkaResources.scala
rename to modules/common-infrastructure/src/test/scala/io/branchtalk/shared/infrastructure/TestKafkaResources.scala
index 18175f1a..4a6931ed 100644
--- a/modules/common-infrastructure/src/it/scala/io/branchtalk/shared/infrastructure/TestKafkaResources.scala
+++ b/modules/common-infrastructure/src/test/scala/io/branchtalk/shared/infrastructure/TestKafkaResources.scala
@@ -3,17 +3,17 @@ package io.branchtalk.shared.infrastructure
import cats.effect.{ Resource, Sync }
import org.apache.kafka.clients.admin.{ AdminClient, AdminClientConfig }
-import scala.jdk.CollectionConverters._
+import scala.jdk.CollectionConverters.*
trait TestKafkaResources extends TestResourcesHelpers {
def kafkaEventBusConfigResource[F[_]: Sync](
testKafkaEventBusConfig: TestKafkaEventBusConfig
- ): Resource[F, KafkaEventBusConfig] =
+ ): Resource[F, KafkaEventBus.BusConfig] =
Resource
.eval(generateRandomSuffix[F])
.flatMap(randomSuffix =>
- Resource.pure[F, KafkaEventBusConfig](testKafkaEventBusConfig.toKafkaEventBusConfig(randomSuffix))
+ Resource.pure[F, KafkaEventBus.BusConfig](testKafkaEventBusConfig.toKafkaEventBusConfig(randomSuffix))
)
.flatTap { cfg =>
Resource.make {
@@ -26,10 +26,12 @@ trait TestKafkaResources extends TestResourcesHelpers {
}
} { client =>
Sync[F].delay {
- try if (client.listTopics().names().get().asScala.contains(cfg.topic.nonEmptyString.value)) {
- client.deleteTopics(List(cfg.topic.nonEmptyString.value).asJavaCollection)
- ()
- } finally client.close()
+ try
+ if (client.listTopics().names().get().asScala.contains(cfg.topic.unwrap)) {
+ client.deleteTopics(List(cfg.topic.unwrap).asJavaCollection)
+ ()
+ }
+ finally client.close()
}
}
}
diff --git a/modules/common-infrastructure/src/test/scala/io/branchtalk/shared/infrastructure/TestPostgresConfig.scala b/modules/common-infrastructure/src/test/scala/io/branchtalk/shared/infrastructure/TestPostgresConfig.scala
new file mode 100644
index 00000000..8dd6917d
--- /dev/null
+++ b/modules/common-infrastructure/src/test/scala/io/branchtalk/shared/infrastructure/TestPostgresConfig.scala
@@ -0,0 +1,29 @@
+package io.branchtalk.shared.infrastructure
+
+import io.scalaland.chimney.dsl.*
+import io.branchtalk.shared.infrastructure.PureconfigSupport.{ *, given }
+
+final case class TestPostgresConfig(
+ url: PostgresDatabase.URL,
+ rootPassword: PostgresDatabase.Password,
+ usernamePrefix: PostgresDatabase.Username,
+ password: PostgresDatabase.Password,
+ schemaPrefix: PostgresDatabase.Schema,
+ domain: PostgresDatabase.Domain,
+ connectionPool: PostgresDatabase.ConnectionPool
+) derives ConfigReader {
+
+ def username(generatedSuffix: String): PostgresDatabase.Username =
+ PostgresDatabase.Username.unsafeMake(usernamePrefix.unwrap + generatedSuffix)
+ def schema(generatedSuffix: String): PostgresDatabase.Schema =
+ PostgresDatabase.Schema.unsafeMake(schemaPrefix.unwrap + generatedSuffix)
+ def migrationOnStart: PostgresDatabase.MigrationOnStart = PostgresDatabase.MigrationOnStart(true)
+
+ def toPostgresConfig(generatedSuffix: String): PostgresDatabase.Config =
+ this
+ .into[PostgresDatabase.Config]
+ .withFieldConst(_.username, username(generatedSuffix))
+ .withFieldConst(_.schema, schema(generatedSuffix))
+ .withFieldConst(_.migrationOnStart, migrationOnStart)
+ .transform
+}
diff --git a/modules/common-infrastructure/src/it/scala/io/branchtalk/shared/infrastructure/TestPostgresResources.scala b/modules/common-infrastructure/src/test/scala/io/branchtalk/shared/infrastructure/TestPostgresResources.scala
similarity index 50%
rename from modules/common-infrastructure/src/it/scala/io/branchtalk/shared/infrastructure/TestPostgresResources.scala
rename to modules/common-infrastructure/src/test/scala/io/branchtalk/shared/infrastructure/TestPostgresResources.scala
index e2334ac6..fad4c028 100644
--- a/modules/common-infrastructure/src/it/scala/io/branchtalk/shared/infrastructure/TestPostgresResources.scala
+++ b/modules/common-infrastructure/src/test/scala/io/branchtalk/shared/infrastructure/TestPostgresResources.scala
@@ -1,33 +1,33 @@
package io.branchtalk.shared.infrastructure
import cats.effect.{ Async, Resource }
-import io.branchtalk.shared.infrastructure.DoobieSupport._
+import io.branchtalk.logging.Logger
+import io.branchtalk.shared.infrastructure.DoobieSupport.{ *, given }
trait TestPostgresResources extends TestResourcesHelpers {
- implicit val logger: DoobieSupport.LogHandler = doobieLogger(getClass)
-
def postgresConfigResource[F[_]: Async](
testPostgresConfig: TestPostgresConfig
- ): Resource[F, PostgresConfig] =
+ ): Resource[F, PostgresDatabase.Config] =
Resource.eval(generateRandomSuffix[F]).flatMap { randomSuffix =>
val schemaCreator = Transactor.fromDriverManager[F](
driver = classOf[org.postgresql.Driver].getName, // driver classname
- url = testPostgresConfig.url.nonEmptyString.value, // connect URL (driver-specific)
+ url = testPostgresConfig.url.unwrap, // connect URL (driver-specific)
user = "postgres", // user
- pass = testPostgresConfig.rootPassword.nonEmptyString.value // password
+ password = testPostgresConfig.rootPassword.unwrap, // password
+ logHandler = Some(doobieLogger[F](Logger.getLoggerFromClass[F](getClass)))
)
val cfg = testPostgresConfig.toPostgresConfig(randomSuffix.toLowerCase)
- val username = Fragment.const(cfg.username.nonEmptyString.value)
- val password = Fragment.const(s"""'${cfg.password.nonEmptyString.value}'""")
- val schema = Fragment.const(cfg.schema.nonEmptyString.value)
+ val username = Fragment.const(cfg.username.unwrap)
+ val password = Fragment.const(s"""'${cfg.password.unwrap}'""")
+ val schema = Fragment.const(cfg.schema.unwrap)
- val createUser = (fr"CREATE USER" ++ username ++ fr"WITH PASSWORD" ++ password).update.run
- val createSchema = (fr"CREATE SCHEMA" ++ schema ++ fr"AUTHORIZATION" ++ username).update.run
+ val createUser = sql"CREATE USER $username WITH PASSWORD $password".update.run
+ val createSchema = sql"CREATE SCHEMA $schema AUTHORIZATION $username".update.run
- val dropSchema = (fr"DROP SCHEMA IF EXISTS" ++ schema ++ fr"CASCADE").update.run
- val dropUser = (fr"DROP ROLE IF EXISTS" ++ username).update.run
+ val dropSchema = sql"DROP SCHEMA IF EXISTS $schema CASCADE".update.run
+ val dropUser = sql"DROP ROLE IF EXISTS $username".update.run
Resource.make {
(createUser >> createSchema >> cfg.pure[ConnectionIO]).transact(schemaCreator)
diff --git a/modules/common-infrastructure/src/it/scala/io/branchtalk/shared/infrastructure/TestResources.scala b/modules/common-infrastructure/src/test/scala/io/branchtalk/shared/infrastructure/TestResources.scala
similarity index 54%
rename from modules/common-infrastructure/src/it/scala/io/branchtalk/shared/infrastructure/TestResources.scala
rename to modules/common-infrastructure/src/test/scala/io/branchtalk/shared/infrastructure/TestResources.scala
index d437e3c4..dfdb188f 100644
--- a/modules/common-infrastructure/src/it/scala/io/branchtalk/shared/infrastructure/TestResources.scala
+++ b/modules/common-infrastructure/src/test/scala/io/branchtalk/shared/infrastructure/TestResources.scala
@@ -1,4 +1,4 @@
package io.branchtalk.shared.infrastructure
-trait TestResources extends TestPostgresResources with TestKafkaResources
+trait TestResources extends TestPostgresResources, TestKafkaResources
object TestResources extends TestResources
diff --git a/modules/common-infrastructure/src/it/scala/io/branchtalk/shared/infrastructure/TestResourcesHelpers.scala b/modules/common-infrastructure/src/test/scala/io/branchtalk/shared/infrastructure/TestResourcesHelpers.scala
similarity index 100%
rename from modules/common-infrastructure/src/it/scala/io/branchtalk/shared/infrastructure/TestResourcesHelpers.scala
rename to modules/common-infrastructure/src/test/scala/io/branchtalk/shared/infrastructure/TestResourcesHelpers.scala
diff --git a/modules/common-macros/src/main/resources/derive.semi.conf b/modules/common-macros/src/main/resources/derive.semi.conf
deleted file mode 100644
index 592df906..00000000
--- a/modules/common-macros/src/main/resources/derive.semi.conf
+++ /dev/null
@@ -1,10 +0,0 @@
-# wire our own type classes for Catnip @Semi
-io.branchtalk.shared.model.ApplicativeFunctor=io.branchtalk.shared.model.ApplicativeFunctor.semi
-io.branchtalk.shared.model.FastEq=io.branchtalk.shared.model.FastEq.semi
-io.branchtalk.shared.model.ShowPretty=io.branchtalk.shared.model.ShowPretty.semi
-# wire Avro4s type classes for Catnip @Semi
-com.sksamuel.avro4s.Decoder=com.sksamuel.avro4s.Decoder.gen
-com.sksamuel.avro4s.Encoder=com.sksamuel.avro4s.Encoder.gen
-com.sksamuel.avro4s.SchemaFor=com.sksamuel.avro4s.SchemaFor.gen
-# wire Pureconfig type classes for Catnip @Semi
-pureconfig.ConfigReader=pureconfig.generic.semiauto.deriveReader
diff --git a/modules/common/src/main/scala/io/branchtalk/ADT.scala b/modules/common/src/main/scala/io/branchtalk/ADT.scala
deleted file mode 100644
index 28eacc78..00000000
--- a/modules/common/src/main/scala/io/branchtalk/ADT.scala
+++ /dev/null
@@ -1,4 +0,0 @@
-package io.branchtalk
-
-// Saves us writing everywhere `sealed trait X extends Product with Serializable`.
-trait ADT extends Product with Serializable
diff --git a/modules/common/src/main/scala/io/branchtalk/logging/CorrelationID.scala b/modules/common/src/main/scala/io/branchtalk/logging/CorrelationID.scala
new file mode 100644
index 00000000..1c9aa286
--- /dev/null
+++ b/modules/common/src/main/scala/io/branchtalk/logging/CorrelationID.scala
@@ -0,0 +1,25 @@
+package io.branchtalk.logging
+
+import cats.{ Order, Show }
+import cats.effect.Sync
+import io.branchtalk.shared.model.UUID
+import neotype.*
+
+type CorrelationID = CorrelationID.Type
+object CorrelationID extends Newtype[String] {
+
+ private val key = "correlation-id"
+
+ def generate[F[_]: Sync](using UUID.Generator): F[CorrelationID] =
+ UUID.create[F].map(_.show).map(unsafeMake)
+
+ def getCurrent[F[_]: MDC]: F[Option[CorrelationID]] = unsafeMakeF[[A] =>> F[Option[A]]](MDC[F].get(key))
+
+ def getCurrentOrGenerate[F[_]: Sync: MDC](using uuidGenerator: UUID.Generator): F[CorrelationID] =
+ getCurrent[F].flatMap(_.fold(generate[F])(_.pure[F]))
+
+ extension (cid: CorrelationID) def updateMDC[F[_]: MDC]: F[Unit] = MDC[F].set(key, cid.unwrap)
+
+ given Show[CorrelationID] = unsafeMakeF[Show](Show[String])
+ given Order[CorrelationID] = unsafeMakeF[Order](Order[String])
+}
diff --git a/modules/common/src/main/scala/io/branchtalk/logging/Logger.scala b/modules/common/src/main/scala/io/branchtalk/logging/Logger.scala
new file mode 100644
index 00000000..6bbd5cb2
--- /dev/null
+++ b/modules/common/src/main/scala/io/branchtalk/logging/Logger.scala
@@ -0,0 +1,12 @@
+package io.branchtalk.logging
+
+import org.typelevel.log4cats.SelfAwareStructuredLogger
+import org.typelevel.log4cats.slf4j.Slf4jLogger
+
+type Logger[F[_]] = SelfAwareStructuredLogger[F]
+val Logger = Slf4jLogger
+
+extension (l: Logger.type) {
+
+ def apply[F[_]](using logger: Logger[F]): Logger[F] = logger
+}
diff --git a/modules/common/src/main/scala/io/branchtalk/logging/MDC.scala b/modules/common/src/main/scala/io/branchtalk/logging/MDC.scala
index 85bb9c5f..fff2b806 100644
--- a/modules/common/src/main/scala/io/branchtalk/logging/MDC.scala
+++ b/modules/common/src/main/scala/io/branchtalk/logging/MDC.scala
@@ -3,15 +3,13 @@ package io.branchtalk.logging
// Abstracts away how we perform MCD from what effect F we use.
trait MDC[F[_]] {
- def ctx: F[MDC.Ctx]
-
- def get(key: String): F[Option[String]]
-
+ def ctx: F[MDC.Ctx]
+ def get(key: String): F[Option[String]]
def set(key: String, value: String): F[Unit]
}
object MDC {
type Ctx = Map[String, String]
- @inline def apply[F[_]](implicit mdc: MDC[F]): MDC[F] = mdc
+ inline def apply[F[_]](using mdc: MDC[F]): MDC[F] = mdc
}
diff --git a/modules/common/src/main/scala/io/branchtalk/logging/RequestID.scala b/modules/common/src/main/scala/io/branchtalk/logging/RequestID.scala
new file mode 100644
index 00000000..9cac163e
--- /dev/null
+++ b/modules/common/src/main/scala/io/branchtalk/logging/RequestID.scala
@@ -0,0 +1,25 @@
+package io.branchtalk.logging
+
+import cats.{ Order, Show }
+import cats.effect.Sync
+import io.branchtalk.shared.model.UUID
+import neotype.*
+
+type RequestID = RequestID.Type
+object RequestID extends Newtype[String] {
+
+ private val key = "request-id"
+
+ def generate[F[_]: Sync](using UUID.Generator): F[RequestID] =
+ UUID.create[F].map(_.show).map(RequestID(_))
+
+ def getCurrent[F[_]: MDC]: F[Option[RequestID]] = unsafeMakeF[[A] =>> F[Option[A]]](MDC[F].get(key))
+
+ def getCurrentOrGenerate[F[_]: Sync: MDC](using UUID.Generator): F[RequestID] =
+ getCurrent[F].flatMap(_.fold(generate[F])(_.pure[F]))
+
+ extension (rid: RequestID) def updateMDC[F[_]: MDC]: F[Unit] = MDC[F].set(key, rid.unwrap)
+
+ given Show[RequestID] = unsafeMakeF[Show](Show[String])
+ given Order[RequestID] = unsafeMakeF[Order](Order[String])
+}
diff --git a/modules/common/src/main/scala/io/branchtalk/logging/package.scala b/modules/common/src/main/scala/io/branchtalk/logging/package.scala
deleted file mode 100644
index 1ed98eb4..00000000
--- a/modules/common/src/main/scala/io/branchtalk/logging/package.scala
+++ /dev/null
@@ -1,52 +0,0 @@
-package io.branchtalk
-
-import cats.{ Order, Show }
-import cats.effect.Sync
-import io.branchtalk.shared.model.UUIDGenerator
-import io.estatico.newtype.macros.newtype
-import io.estatico.newtype.ops._
-
-package object logging {
-
- // utilities for generating CorrelationID and RequestID
-
- @newtype final case class CorrelationID(show: String) {
-
- def updateMDC[F[_]: MDC]: F[Unit] = MDC[F].set(CorrelationID.key, show)
- }
- object CorrelationID {
-
- private val key = "correlation-id"
-
- def generate[F[_]: Sync](implicit uuidGenerator: UUIDGenerator): F[CorrelationID] =
- uuidGenerator.create[F].map(_.toString).map(CorrelationID(_))
-
- def getCurrent[F[_]: MDC]: F[Option[CorrelationID]] = MDC[F].get(key).coerce
-
- def getCurrentOrGenerate[F[_]: Sync: MDC](implicit uuidGenerator: UUIDGenerator): F[CorrelationID] =
- getCurrent[F].flatMap(_.fold(generate[F])(_.pure[F]))
-
- implicit val show: Show[CorrelationID] = Show[String].coerce
- implicit val order: Order[CorrelationID] = Order[String].coerce
- }
-
- @newtype final case class RequestID(show: String) {
-
- def updateMDC[F[_]: MDC]: F[Unit] = MDC[F].set(RequestID.key, show)
- }
- object RequestID {
-
- private val key = "request-id"
-
- def generate[F[_]: Sync](implicit uuidGenerator: UUIDGenerator): F[RequestID] =
- uuidGenerator.create[F].map(_.toString).map(RequestID(_))
-
- def getCurrent[F[_]: MDC]: F[Option[RequestID]] = MDC[F].get(key).coerce
-
- def getCurrentOrGenerate[F[_]: Sync: MDC](implicit uuidGenerator: UUIDGenerator): F[RequestID] =
- getCurrent[F].flatMap(_.fold(generate[F])(_.pure[F]))
-
- implicit val show: Show[RequestID] = Show[String].coerce
- implicit val order: Order[RequestID] = Order[String].coerce
- }
-}
diff --git a/modules/common/src/main/scala/io/branchtalk/shared/model/ApplicativeTraverse.scala b/modules/common/src/main/scala/io/branchtalk/shared/model/ApplicativeTraverse.scala
index 6ee1d732..7ed74dda 100644
--- a/modules/common/src/main/scala/io/branchtalk/shared/model/ApplicativeTraverse.scala
+++ b/modules/common/src/main/scala/io/branchtalk/shared/model/ApplicativeTraverse.scala
@@ -3,21 +3,17 @@ package io.branchtalk.shared.model
import cats.{ Applicative, Eval, Traverse }
// because, apparently, you cannot have both at once since both extends Functor
-trait ApplicativeTraverse[F[_]] extends Applicative[F] with Traverse[F]
+trait ApplicativeTraverse[F[_]] extends Applicative[F], Traverse[F]
object ApplicativeTraverse {
- def semi[F[_]: Applicative: Traverse]: ApplicativeTraverse[F] = {
+ def derived[F[_]: Applicative: Traverse]: ApplicativeTraverse[F] = {
val a = Applicative[F]
val t = Traverse[F]
new ApplicativeTraverse[F] {
- override def pure[A](x: A): F[A] = a.pure(x)
-
+ override def pure[A](x: A): F[A] = a.pure(x)
override def ap[A, B](ff: F[A => B])(fa: F[A]): F[B] = a.ap(ff)(fa)
-
override def traverse[G[_], A, B](fa: F[A])(f: A => G[B])(implicit G: Applicative[G]): G[F[B]] = t.traverse(fa)(f)
-
- override def foldLeft[A, B](fa: F[A], b: B)(f: (B, A) => B): B = t.foldLeft(fa, b)(f)
-
+ override def foldLeft[A, B](fa: F[A], b: B)(f: (B, A) => B): B = t.foldLeft(fa, b)(f)
override def foldRight[A, B](fa: F[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = t.foldRight(fa, lb)(f)
}
}
diff --git a/modules/common/src/main/scala/io/branchtalk/shared/model/AvroSerialization.scala b/modules/common/src/main/scala/io/branchtalk/shared/model/AvroSerialization.scala
deleted file mode 100644
index 28d564e1..00000000
--- a/modules/common/src/main/scala/io/branchtalk/shared/model/AvroSerialization.scala
+++ /dev/null
@@ -1,86 +0,0 @@
-package io.branchtalk.shared.model
-
-import java.io.{ ByteArrayInputStream, ByteArrayOutputStream }
-import cats.{ Eq, Show }
-import cats.effect.{ Resource, Sync, SyncIO }
-import com.sksamuel.avro4s._
-import io.branchtalk.ADT
-import io.branchtalk.shared.model.AvroSerialization.DeserializationResult
-import io.scalaland.catnip.Semi
-import io.scalaland.chimney.TransformerFSupport
-
-import scala.collection.compat.Factory
-import scala.util.{ Failure, Success }
-
-// Missing helpers for serializations and deserialization with some API saner than byte array streams.
-@Semi(FastEq, ShowPretty) sealed trait DeserializationError extends ADT
-object DeserializationError {
- final case class DecodingError(badValue: String, error: Throwable) extends DeserializationError
-
- implicit val eitherTransformerSupport: TransformerFSupport[DeserializationResult] =
- new TransformerFSupport[DeserializationResult] {
-
- override def pure[A](value: A): DeserializationResult[A] = value.asRight
-
- override def product[A, B](
- fa: DeserializationResult[A],
- fb: => DeserializationResult[B]
- ): DeserializationResult[(A, B)] = for { a <- fa; b <- fb } yield (a, b)
-
- override def map[A, B](fa: DeserializationResult[A], f: A => B): DeserializationResult[B] = fa.map(f)
-
- override def traverse[M, A, B](it: Iterator[A], f: A => DeserializationResult[B])(implicit
- fac: Factory[B, M]
- ): DeserializationResult[M] = it.toList.traverse(f).map(fac.fromSpecific)
- }
-
- implicit private def showThrowable: Show[Throwable] = _.getMessage
- implicit private def eqThrowable: Eq[Throwable] = _.getMessage === _.getMessage
-}
-
-object AvroSerialization {
-
- private val logger = com.typesafe.scalalogging.Logger(getClass)
-
- type DeserializationResult[+A] = Either[DeserializationError, A]
-
- def serialize[F[_]: Sync, A: Encoder](value: A): F[Array[Byte]] =
- Resource.fromAutoCloseable(Sync[F].delay(new ByteArrayOutputStream())).use { baos =>
- Sync[F].delay {
- val aos = AvroOutputStream.json[A].to(baos).build()
- aos.write(value)
- aos.close()
- aos.flush()
- baos.toByteArray
- }
- }
-
- @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
- def deserialize[F[_]: Sync, A: Decoder: SchemaFor](arr: Array[Byte]): F[DeserializationResult[A]] =
- Resource.fromAutoCloseable(Sync[F].delay(new ByteArrayInputStream(arr))).use { bais =>
- Sync[F]
- .delay {
- AvroInputStream
- .json[A]
- .from(bais)
- .build(SchemaFor[A].schema)
- .asInstanceOf[AvroJsonInputStream[A]]
- .singleEntity match {
- case Success(value) =>
- value.asRight[DeserializationError]
- case Failure(error) =>
- DeserializationError.DecodingError("Failed to extract Avro message", error).asLeft[A]
- }
- }
- .handleError { error: Throwable =>
- logger.error(s"Avro deserialization error for '${new String(arr, branchtalkCharset)}'", error)
- DeserializationError.DecodingError(new String(arr, branchtalkCharset), error).asLeft[A]
- }
- }
-
- def serializeUnsafe[A: Encoder](value: A): Array[Byte] =
- serialize[SyncIO, A](value).unsafeRunSync()
-
- def deserializeUnsafe[A: Decoder: SchemaFor](arr: Array[Byte]): DeserializationResult[A] =
- deserialize[SyncIO, A](arr).unsafeRunSync()
-}
diff --git a/modules/common/src/main/scala/io/branchtalk/shared/model/AvroSupport.scala b/modules/common/src/main/scala/io/branchtalk/shared/model/AvroSupport.scala
deleted file mode 100644
index bdc105a5..00000000
--- a/modules/common/src/main/scala/io/branchtalk/shared/model/AvroSupport.scala
+++ /dev/null
@@ -1,106 +0,0 @@
-package io.branchtalk.shared.model
-
-import java.net.URI
-import java.util
-
-import cats.Id
-import cats.data.{ NonEmptyList, NonEmptyVector }
-import com.sksamuel.avro4s._
-import eu.timepit.refined.api.{ RefType, Validate }
-import io.estatico.newtype.Coercible
-import org.apache.avro.Schema
-
-import scala.jdk.CollectionConverters._
-
-object AvroSupport {
-
- // newtype - order of implicits is necessary (swapping them would break derivations, so we can't use typeclass syntax)
-
- implicit def coercibleDecoder[R, N](implicit ev: Coercible[R, N], R: Decoder[R]): Decoder[N] =
- Coercible.unsafeWrapMM[Decoder, Id, R, N].apply(R)
- implicit def coercibleEncoder[R, N](implicit ev: Coercible[R, N], R: Encoder[R]): Encoder[N] =
- Coercible.unsafeWrapMM[Encoder, Id, R, N].apply(R)
- implicit def coercibleSchemaFor[R, N](implicit ev: Coercible[R, N], R: SchemaFor[R]): SchemaFor[N] =
- Coercible.unsafeWrapMM[SchemaFor, Id, R, N].apply(R)
-
- // refined (redirections)
-
- implicit def refinedSchemaFor[T: SchemaFor, P, F[_, _]: RefType]: SchemaFor[F[T, P]] =
- refined.refinedSchemaFor[T, P, F]
-
- implicit def refinedEncoder[T: Encoder, P, F[_, _]: RefType]: Encoder[F[T, P]] =
- refined.refinedEncoder[T, P, F]
-
- implicit def refinedDecoder[T: Decoder, P: Validate[T, *], F[_, _]: RefType]: Decoder[F[T, P]] =
- refined.refinedDecoder[T, P, F]
-
- // cats (copy-paste as cats module isn't released)
-
- implicit def nonEmptyListSchemaFor[T: SchemaFor]: SchemaFor[NonEmptyList[T]] =
- SchemaFor(Schema.createArray(SchemaFor[T].schema))
-
- implicit def nonEmptyVectorSchemaFor[T: SchemaFor]: SchemaFor[NonEmptyVector[T]] =
- SchemaFor(Schema.createArray(SchemaFor[T].schema))
-
- implicit def nonEmptyListEncoder[T: Encoder]: Encoder[NonEmptyList[T]] =
- new Encoder[NonEmptyList[T]] {
-
- val schemaFor: SchemaFor[NonEmptyList[T]] = nonEmptyListSchemaFor(Encoder[T].schemaFor)
-
- @SuppressWarnings(Array("org.wartremover.warts.Equals", "org.wartremover.warts.Null"))
- override def encode(ts: NonEmptyList[T]): java.util.List[AnyRef] = {
- require(schema != null)
- ts.map(Encoder[T].encode).toList.asJava
- }
- }
-
- implicit def nonEmptyVectorEncoder[T: Encoder]: Encoder[NonEmptyVector[T]] =
- new Encoder[NonEmptyVector[T]] {
-
- val schemaFor: SchemaFor[NonEmptyVector[T]] = nonEmptyVectorSchemaFor(Encoder[T].schemaFor)
-
- @SuppressWarnings(Array("org.wartremover.warts.Equals", "org.wartremover.warts.Null"))
- override def encode(ts: NonEmptyVector[T]): java.util.List[AnyRef] = {
- require(schema != null)
- ts.map(Encoder[T].encode).toVector.asJava
- }
- }
-
- implicit def nonEmptyListDecoder[T: Decoder]: Decoder[NonEmptyList[T]] =
- new Decoder[NonEmptyList[T]] {
-
- val schemaFor: SchemaFor[NonEmptyList[T]] = nonEmptyListSchemaFor(Decoder[T].schemaFor)
-
- @SuppressWarnings(Array("org.wartremover.warts.ToString"))
- override def decode(value: Any): NonEmptyList[T] = value match {
- case array: Array[_] => NonEmptyList.fromListUnsafe(array.toList.map(Decoder[T].decode))
- case list: util.Collection[_] => NonEmptyList.fromListUnsafe(list.asScala.map(Decoder[T].decode).toList)
- case other => sys.error("Unsupported type " + other.toString)
- }
- }
-
- implicit def nonEmptyVectorDecoder[T: Decoder]: Decoder[NonEmptyVector[T]] =
- new Decoder[NonEmptyVector[T]] {
-
- val schemaFor: SchemaFor[NonEmptyVector[T]] = nonEmptyVectorSchemaFor(Decoder[T].schemaFor)
-
- @SuppressWarnings(Array("org.wartremover.warts.ToString"))
- override def decode(value: Any): NonEmptyVector[T] = value match {
- case array: Array[_] => NonEmptyVector.fromVectorUnsafe(array.toVector.map(Decoder[T].decode))
- case list: util.Collection[_] => NonEmptyVector.fromVectorUnsafe(list.asScala.map(Decoder[T].decode).toVector)
- case other => sys.error("Unsupported type " + other.toString)
- }
- }
-
- // custom types
-
- implicit val uriSchema: SchemaFor[URI] = SchemaFor[String].forType[URI]
- implicit val uriEncoder: Encoder[URI] = new Encoder[URI] {
- override def encode(value: URI): AnyRef = Encoder[String].encode(value.toString)
- override def schemaFor: SchemaFor[URI] = uriSchema
- }
- implicit val uriDecoder: Decoder[URI] = new Decoder[URI] {
- override def decode(value: Any): URI = URI.create(Decoder[String].decode(value))
- override def schemaFor: SchemaFor[URI] = uriSchema
- }
-}
diff --git a/modules/common/src/main/scala/io/branchtalk/shared/model/CodePosition.scala b/modules/common/src/main/scala/io/branchtalk/shared/model/CodePosition.scala
index b81a9cf7..3cbebb7c 100644
--- a/modules/common/src/main/scala/io/branchtalk/shared/model/CodePosition.scala
+++ b/modules/common/src/main/scala/io/branchtalk/shared/model/CodePosition.scala
@@ -1,16 +1,15 @@
package io.branchtalk.shared.model
-import io.scalaland.catnip.Semi
-
// Useful for generating the position in source code for debugging.
-@Semi(FastEq, ShowPretty) final case class CodePosition(
+final case class CodePosition(
file: String,
line: Int,
context: String
-)
+) derives FastEq,
+ ShowPretty
object CodePosition {
- implicit def providePosition(implicit
+ given providePosition(using
file: sourcecode.File,
line: sourcecode.Line,
enclosing: sourcecode.Enclosing.Machine
diff --git a/modules/common/src/main/scala/io/branchtalk/shared/model/CommonError.scala b/modules/common/src/main/scala/io/branchtalk/shared/model/CommonError.scala
index fbf2afbf..d3bc68de 100644
--- a/modules/common/src/main/scala/io/branchtalk/shared/model/CommonError.scala
+++ b/modules/common/src/main/scala/io/branchtalk/shared/model/CommonError.scala
@@ -1,27 +1,39 @@
package io.branchtalk.shared.model
import cats.data.NonEmptyList
-import io.branchtalk.ADT
-import io.scalaland.catnip.Semi
+import neotype.*
// Defines errors as ADT but also Throwable, so that we can extract during handling it and pattern-match all cases.
-@Semi(FastEq, ShowPretty) sealed trait CommonError extends Exception with ADT {
+enum CommonError extends Exception derives FastEq, ShowPretty {
+ case InvalidCredentials(codePosition: CodePosition)
+ case InsufficientPermissions(msg: String, codePosition: CodePosition)
+ case NotFound(entity: String, id: UUID, codePosition: CodePosition)
+ case ParentNotExist(entity: String, id: UUID, codePosition: CodePosition)
+ case ValidationFailed(errors: NonEmptyList[String], codePosition: CodePosition)
+
val codePosition: CodePosition
+
+ override def getMessage: String = this match {
+ case InvalidCredentials(codePosition) => show"Invalid credentials at: $codePosition"
+ case InsufficientPermissions(msg, codePosition) => show"Insufficient permissions at: $codePosition\n$msg"
+ case NotFound(entity, id, codePosition) => show"Entity $entity id=$id not found at: $codePosition"
+ case ParentNotExist(entity, id, codePosition) => show"Entity's parent $entity id=$id not exist at: $codePosition"
+ case ValidationFailed(errors, codePosition) =>
+ show"Validation failed at: $codePosition:\n${errors.mkString_("- ", "\n", "")}"
+ }
+
override def toString: String = this.show
}
object CommonError {
- final case class InvalidCredentials(codePosition: CodePosition) extends CommonError
- final case class InsufficientPermissions(msg: String, codePosition: CodePosition) extends CommonError {
- override def getMessage: String = msg
- }
- final case class NotFound(entity: String, id: ID[_], codePosition: CodePosition) extends CommonError {
- override def getMessage: String = show"Entity $entity id=$id not found at: $codePosition"
- }
- final case class ParentNotExist(entity: String, id: ID[_], codePosition: CodePosition) extends CommonError {
- override def getMessage: String = show"Entity's parent $entity id=$id not exist at: $codePosition"
- }
- final case class ValidationFailed(errors: NonEmptyList[String], codePosition: CodePosition) extends CommonError {
- override def getMessage: String =
- show"Validation failed at: $codePosition:\n${errors.mkString_("- ", "\n", "")}"
- }
+
+ def invalidCredentials(using codePosition: CodePosition): CommonError =
+ InvalidCredentials(codePosition)
+ def insufficientPermissions(msg: String)(using codePosition: CodePosition): CommonError =
+ InsufficientPermissions(msg, codePosition)
+ def notFound[Entity](entity: String, id: ID[Entity])(using codePosition: CodePosition): CommonError =
+ NotFound(entity, id.unwrap, codePosition)
+ def parentNotExist[Entity](entity: String, id: ID[Entity])(using codePosition: CodePosition): CommonError =
+ ParentNotExist(entity, id.unwrap, codePosition)
+ def validationFailed(error: String, errors: String*)(using codePosition: CodePosition): CommonError =
+ ValidationFailed(NonEmptyList(error, errors.toList), codePosition)
}
diff --git a/modules/common/src/main/scala/io/branchtalk/shared/model/FastEq.scala b/modules/common/src/main/scala/io/branchtalk/shared/model/FastEq.scala
index 98e585d1..83cb0214 100644
--- a/modules/common/src/main/scala/io/branchtalk/shared/model/FastEq.scala
+++ b/modules/common/src/main/scala/io/branchtalk/shared/model/FastEq.scala
@@ -1,26 +1,20 @@
package io.branchtalk.shared.model
import cats.Eq
-import magnolia1._
-
-import scala.language.experimental.macros
+import magnolia1.*
// Custom implementation of Eq which relies on Magnolia for derivation as opposed to Kittens' version.
trait FastEq[T] extends Eq[T]
-object FastEq extends FastEqLowLevel {
- type Typeclass[T] = FastEq[T]
-
- def join[T](caseClass: ReadOnlyCaseClass[Typeclass, T]): Typeclass[T] =
- (x, y) => caseClass.parameters.forall(p => p.typeclass.eqv(p.dereference(x), p.dereference(y)))
+object FastEq extends Derivation[FastEq], FastEqLowLevel {
- def split[T](sealedTrait: SealedTrait[Typeclass, T]): Typeclass[T] =
- (x, y) => sealedTrait.split(x)(sub => sub.cast.isDefinedAt(y) && sub.typeclass.eqv(sub.cast(x), sub.cast(y)))
+ def join[T](caseClass: CaseClass[FastEq, T]): FastEq[T] =
+ (x, y) => caseClass.parameters.forall(p => p.typeclass.eqv(p.deref(x), p.deref(y)))
- def semi[T]: Typeclass[T] = macro Magnolia.gen[T]
+ def split[T](sealedTrait: SealedTrait[FastEq, T]): FastEq[T] =
+ (x, y) => sealedTrait.choose(x)(sub => sub.cast.isDefinedAt(y) && sub.typeclass.eqv(sub.cast(x), sub.cast(y)))
}
trait FastEqLowLevel { self: FastEq.type =>
- implicit def liftEq[T](implicit normalEq: Eq[T]): FastEq[T] =
- (x: T, y: T) => normalEq.eqv(x, y)
+ given liftEq[T](using normalEq: Eq[T]): FastEq[T] = (x: T, y: T) => normalEq.eqv(x, y)
}
diff --git a/modules/common/src/main/scala/io/branchtalk/shared/model/ID.scala b/modules/common/src/main/scala/io/branchtalk/shared/model/ID.scala
new file mode 100644
index 00000000..b11af722
--- /dev/null
+++ b/modules/common/src/main/scala/io/branchtalk/shared/model/ID.scala
@@ -0,0 +1,17 @@
+package io.branchtalk.shared.model
+
+import cats.{ Order, Show }
+import cats.effect.Sync
+import neotype.*
+
+type ID[Entity] = ID.Type[Entity]
+object ID extends NewtypeT[UUID] {
+
+ def unapply[Entity](entity: ID[Entity]): Some[UUID] = Some(entity.unwrap)
+ def create[F[_]: Sync, Entity](using UUID.Generator): F[ID[Entity]] = UUID.create[F].map(unsafeMake[Entity])
+ def parse[F[_]: Sync, Entity](string: String)(using UUID.Generator): F[ID[Entity]] =
+ UUID.parse[F](string).map(unsafeMake[Entity])
+
+ given [Entity]: Show[ID[Entity]] = unsafeMakeF[Show, Entity](Show[UUID])
+ given [Entity]: Order[ID[Entity]] = unsafeMakeF[Order, Entity](Order[UUID])
+}
diff --git a/modules/common/src/main/scala/io/branchtalk/shared/model/OptionUpdatable.scala b/modules/common/src/main/scala/io/branchtalk/shared/model/OptionUpdatable.scala
deleted file mode 100644
index b1c4d29f..00000000
--- a/modules/common/src/main/scala/io/branchtalk/shared/model/OptionUpdatable.scala
+++ /dev/null
@@ -1,39 +0,0 @@
-package io.branchtalk.shared.model
-
-import cats.{ Applicative, Traverse }
-import io.scalaland.catnip.Semi
-
-// Express the intent that something should be updated, erased or kept better than Option[Either[Unit, *]].
-@Semi(ShowPretty, FastEq) sealed trait OptionUpdatable[+A] {
-
- def fold[B](set: A => B, keep: => B, erase: => B): B = this match {
- case OptionUpdatable.Set(value) => set(value)
- case OptionUpdatable.Erase => keep
- case OptionUpdatable.Keep => erase
- }
-
- def toOptionEither: Option[Either[Unit, A]] = this match {
- case OptionUpdatable.Set(value) => Some(Right(value))
- case OptionUpdatable.Erase => Some(Left(()))
- case OptionUpdatable.Keep => None
- }
-}
-object OptionUpdatable {
- final case class Set[+A](value: A) extends OptionUpdatable[A]
- case object Erase extends OptionUpdatable[Nothing]
- case object Keep extends OptionUpdatable[Nothing]
-
- def setFromOption[A](option: Option[A]): OptionUpdatable[A] = option.fold[OptionUpdatable[A]](Erase)(Set(_))
-
- private val applicative: Applicative[OptionUpdatable] = new Applicative[OptionUpdatable] {
- override def pure[A](a: A): OptionUpdatable[A] = Set(a)
- override def ap[A, B](ff: OptionUpdatable[A => B])(fa: OptionUpdatable[A]): OptionUpdatable[B] = (ff, fa) match {
- case (Set(f), Set(a)) => Set(f(a))
- case (Erase, _) => Erase
- case (_, Erase) => Erase
- case _ => Keep
- }
- }
- private val traverse: Traverse[OptionUpdatable] = cats.derived.semiauto.traverse[OptionUpdatable]
- implicit val appTraverse: ApplicativeTraverse[OptionUpdatable] = ApplicativeTraverse.semi(applicative, traverse)
-}
diff --git a/modules/common/src/main/scala/io/branchtalk/shared/model/Paginated.scala b/modules/common/src/main/scala/io/branchtalk/shared/model/Paginated.scala
index 3f3cd72a..df877cb8 100644
--- a/modules/common/src/main/scala/io/branchtalk/shared/model/Paginated.scala
+++ b/modules/common/src/main/scala/io/branchtalk/shared/model/Paginated.scala
@@ -1,13 +1,34 @@
package io.branchtalk.shared.model
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.numeric.NonNegative
+import cats.{ Order, Show }
+import neotype.*
-final case class Paginated[+Entity](entities: List[Entity], nextOffset: Option[Long Refined NonNegative]) {
+final case class Paginated[+Entity](
+ entities: List[Entity],
+ nextOffset: Option[Paginated.Offset]
+) {
def map[B](f: Entity => B): Paginated[B] = Paginated(entities.map(f), nextOffset)
}
object Paginated {
def empty[Entity]: Paginated[Entity] = Paginated(entities = List.empty, nextOffset = None)
+
+ type Offset = Offset.Type
+ object Offset extends Newtype[Long] {
+
+ override inline def validate(input: Long): Boolean = input >= 0L
+
+ given Show[Offset] = unsafeMakeF[Show](Show[Long])
+ given Order[Offset] = unsafeMakeF[Order](Order[Long])
+ }
+
+ type Limit = Limit.Type
+ object Limit extends Newtype[Int] {
+
+ override inline def validate(input: Int): Boolean = input > 0
+
+ given Show[Limit] = unsafeMakeF[Show](Show[Int])
+ given Order[Limit] = unsafeMakeF[Order](Order[Int])
+ }
}
diff --git a/modules/common/src/main/scala/io/branchtalk/shared/model/ParseNewtype.scala b/modules/common/src/main/scala/io/branchtalk/shared/model/ParseNewtype.scala
new file mode 100644
index 00000000..bd331266
--- /dev/null
+++ b/modules/common/src/main/scala/io/branchtalk/shared/model/ParseNewtype.scala
@@ -0,0 +1,22 @@
+package io.branchtalk.shared.model
+
+import cats.data.NonEmptyList
+import cats.effect.Sync
+import neotype.*
+
+object ParseNewtype {
+
+ def apply[F[_]]: ApplyF[F] = new ApplyF[F]
+
+ class ApplyF[F[_]] {
+ def parse[P]: ApplyFP[F, P] = new ApplyFP[F, P]
+ }
+
+ class ApplyFP[F[_], P] {
+ def apply[T](t: T)(using F: Sync[F], newtype: Newtype.WithType[T, P], codePosition: CodePosition): F[P] = F.defer {
+ F.fromEither {
+ newtype.make(t).leftMap(msg => CommonError.ValidationFailed(NonEmptyList.one(msg), codePosition))
+ }
+ }
+ }
+}
diff --git a/modules/common/src/main/scala/io/branchtalk/shared/model/ParseRefined.scala b/modules/common/src/main/scala/io/branchtalk/shared/model/ParseRefined.scala
deleted file mode 100644
index a56fe2c3..00000000
--- a/modules/common/src/main/scala/io/branchtalk/shared/model/ParseRefined.scala
+++ /dev/null
@@ -1,23 +0,0 @@
-package io.branchtalk.shared.model
-
-import cats.effect.Sync
-import eu.timepit.refined._
-import eu.timepit.refined.api.{ Refined, Validate }
-
-object ParseRefined {
-
- def apply[F[_]]: ApplyF[F] = new ApplyF[F]
-
- class ApplyF[F[_]] {
- def parse[P]: ApplyFP[F, P] = new ApplyFP[F, P]
- }
-
- class ApplyFP[F[_], P] {
- def apply[T](t: T)(implicit F: Sync[F], validate: Validate[T, P]): F[T Refined P] =
- F.defer {
- F.fromEither {
- refineV[P](t).leftMap(msg => new Throwable(msg))
- }
- }
- }
-}
diff --git a/modules/common/src/main/scala/io/branchtalk/shared/model/SensitiveData.scala b/modules/common/src/main/scala/io/branchtalk/shared/model/SensitiveData.scala
index 0a3bb340..d459a924 100644
--- a/modules/common/src/main/scala/io/branchtalk/shared/model/SensitiveData.scala
+++ b/modules/common/src/main/scala/io/branchtalk/shared/model/SensitiveData.scala
@@ -2,100 +2,92 @@ package io.branchtalk.shared.model
import cats.{ Eq, Show }
import com.sksamuel.avro4s.{ Decoder, Encoder, SchemaFor }
-import enumeratum._
+import enumeratum.*
import io.branchtalk.shared.model.AvroSerialization.DeserializationResult
-import io.estatico.newtype.macros.newtype
-import io.estatico.newtype.ops._
-import io.scalaland.catnip.Semi
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
+import neotype.*
import scala.collection.compat.immutable.ArraySeq
import scala.util.{ Random, Try }
// Express intent that some data should not be stored as unencrypted format.
-@Semi(Decoder, Encoder, SchemaFor) final case class SensitiveData[A](value: A) {
+final case class SensitiveData[A](value: A) derives FastEq, Decoder, Encoder, SchemaFor {
def encrypt(
- algorithm: SensitiveData.Algorithm,
- key: SensitiveData.Key
- )(implicit encoder: Encoder[A]): SensitiveData.Encrypted[A] = algorithm.encrypt[A](this, key)
+ algorithm: SensitiveData.Algorithm,
+ key: SensitiveData.Key
+ )(using Encoder[A], SchemaFor[A]): SensitiveData.Encrypted[A] = algorithm.encrypt[A](this, key)
override def toString: String = "SENSITIVE DATA"
}
object SensitiveData {
- @newtype final case class Encrypted[A](bytes: ArraySeq[Byte]) {
+ type Encrypted[A] = Encrypted.Type[A]
+ object Encrypted extends NewtypeT[ArraySeq[Byte]] {
- def decrypt(
- algorithm: SensitiveData.Algorithm,
- key: SensitiveData.Key
- )(implicit decoder: Decoder[A], schemaFor: SchemaFor[A]): DeserializationResult[SensitiveData[A]] =
- algorithm.decrypt[A](this, key)
- }
- object Encrypted {
-
- implicit def show[A]: Show[Encrypted[A]] = Show.wrap(_ => "ENCRYPTED")
- implicit def eq[A]: Eq[Encrypted[A]] = Eq.by(_.bytes)
-
- implicit def decoder[A]: Decoder[Encrypted[A]] =
- Decoder[Array[Byte]].map(ArraySeq.from(_)).coerce[Decoder[Encrypted[A]]]
- implicit def encoder[A]: Encoder[Encrypted[A]] =
- Encoder[Array[Byte]].comap[ArraySeq[Byte]](_.toArray).coerce[Encoder[Encrypted[A]]]
- implicit def schemaFor[A]: SchemaFor[Encrypted[A]] =
- SchemaFor[Array[Byte]].forType[ArraySeq[Byte]].coerce[SchemaFor[Encrypted[A]]]
- }
+ extension [A](enc: Encrypted[A]) {
+ def decrypt(
+ algorithm: SensitiveData.Algorithm,
+ key: SensitiveData.Key
+ )(using Decoder[A], SchemaFor[A]): DeserializationResult[SensitiveData[A]] =
+ algorithm.decrypt[A](enc, key)
+ }
- @newtype final case class Key(bytes: ArraySeq[Byte])
- object Key {
+ given [A]: Decoder[Encrypted[A]] = unsafeMakeF[Decoder, A](Decoder[Array[Byte]].map(ArraySeq.from))
+ given [A]: Encoder[Encrypted[A]] =
+ unsafeMakeF[Encoder, A](Encoder[Array[Byte]].contramap[ArraySeq[Byte]](_.toArray))
+ given [A]: SchemaFor[Encrypted[A]] = SchemaFor[Array[Byte]].forType[Encrypted[A]]
- implicit val show: Show[Key] = Show.wrap(_ => "KEY")
- implicit val eq: Eq[Key] = Eq.by(_.bytes)
+ given [A]: Show[Encrypted[A]] = _ => "ENCRYPTED"
+ given [A]: Eq[Encrypted[A]] = Eq.by(_.unwrap)
}
- sealed trait Algorithm extends EnumEntry with EnumEntry.Hyphencase {
-
- def generateKey(): Key
-
- // Avro4s fails to correctly serialize-then-deserialize primitives so we have to use wrapper (GenericRecord schema)
-
- def encrypt[A: Encoder](value: SensitiveData[A], key: Key): Encrypted[A]
+ type Key = Key.Type
+ object Key extends Newtype[ArraySeq[Byte]] {
- def decrypt[A: Decoder: SchemaFor](encrypted: Encrypted[A], key: Key): DeserializationResult[SensitiveData[A]]
+ given Show[Key] = _ => "KEY"
+ given Eq[Key] = Eq.by(_.unwrap)
}
- object Algorithm extends Enum[Algorithm] {
- private def getCipher(name: String)(key: Key, mode: Int) =
- Cipher.getInstance(name).tap(_.init(mode, new SecretKeySpec(key.bytes.toArray, name)))
+ enum Algorithm extends EnumEntry, EnumEntry.Hyphencase {
+ case Blowfish
- case object Blowfish extends Algorithm {
+ import Algorithm.*
- private val defaultKeySize = 32
- private val blowfishCipher = getCipher("Blowfish") _
+ final def generateKey(): Key = this match {
+ case Blowfish => Key(ArraySeq.from(Random.nextBytes(defaultKeySize)))
+ }
- override def generateKey(): Key = Key(ArraySeq.from(Random.nextBytes(defaultKeySize)))
+ // Avro4s fails to correctly serialize-then-deserialize primitives so we have to use wrapper (GenericRecord schema)
- override def encrypt[A: Encoder](value: SensitiveData[A], key: Key): Encrypted[A] = {
+ final def encrypt[A: Encoder: SchemaFor](value: SensitiveData[A], key: Key): Encrypted[A] = this match {
+ case Blowfish =>
val cipher = blowfishCipher(key, Cipher.ENCRYPT_MODE)
AvroSerialization.serializeUnsafe(value).pipe(cipher.doFinal).pipe(ArraySeq.from(_)).pipe(Encrypted(_))
- }
+ }
- override def decrypt[A: Decoder: SchemaFor](
- encrypted: Encrypted[A],
- key: Key
- ): DeserializationResult[SensitiveData[A]] = {
+ final def decrypt[A: Decoder: SchemaFor](
+ encrypted: Encrypted[A],
+ key: Key
+ ): DeserializationResult[SensitiveData[A]] = this match {
+ case Blowfish =>
val cipher = blowfishCipher(key, Cipher.DECRYPT_MODE)
- Try(cipher.doFinal(encrypted.bytes.toArray)).toEither.left
+ Try(cipher.doFinal(encrypted.unwrap.toArray)).toEither.left
.map(DeserializationError.DecodingError("SensitiveData decoding error", _))
.flatMap(AvroSerialization.deserializeUnsafe[SensitiveData[A]](_))
- }
}
+ }
+ object Algorithm {
- def default: Algorithm = Blowfish // TODO: make configurable
+ private def getCipher(name: String)(key: Key, mode: Int) =
+ Cipher.getInstance(name).tap(_.init(mode, new SecretKeySpec(key.unwrap.toArray, name)))
- val values = findValues
+ private val defaultKeySize = 32
+ private val blowfishCipher = getCipher("Blowfish") _
+
+ def default: Algorithm = Blowfish // TODO: make configurable
}
- implicit def show[A]: Show[A] = Show.wrap(_ => "SENSITIVE DATA")
- implicit def eq[A: Eq]: Eq[SensitiveData[A]] = Eq.by(_.value)
+ given [A]: Show[A] = _ => "SENSITIVE DATA"
}
diff --git a/modules/common/src/main/scala/io/branchtalk/shared/model/ShowPretty.scala b/modules/common/src/main/scala/io/branchtalk/shared/model/ShowPretty.scala
index f7c81435..fc557aae 100644
--- a/modules/common/src/main/scala/io/branchtalk/shared/model/ShowPretty.scala
+++ b/modules/common/src/main/scala/io/branchtalk/shared/model/ShowPretty.scala
@@ -1,14 +1,13 @@
package io.branchtalk.shared.model
import cats.Show
-import magnolia1._
-
-import scala.language.experimental.macros
+import magnolia1.*
// Custom implementation of ShowPretty which relies on Magnolia for derivation as opposed to Kittens' version.
+@SuppressWarnings(Array("org.wartremover.warts.MutableDataStructures"))
trait ShowPretty[T] extends Show[T] {
- def show(t: T): String = showPretty(t).toString()
+ final def show(t: T): String = showPretty(t).result()
def showPretty(
t: T,
@@ -18,34 +17,33 @@ trait ShowPretty[T] extends Show[T] {
): StringBuilder
}
-object ShowPretty extends ShowPrettyLowLevel {
- type Typeclass[T] = ShowPretty[T]
+@SuppressWarnings(Array("org.wartremover.warts.MutableDataStructures"))
+object ShowPretty extends Derivation[ShowPretty], ShowPrettyLowLevel {
- def join[T](caseClass: ReadOnlyCaseClass[Typeclass, T]): Typeclass[T] =
- (t: T, sb: StringBuilder, indentWith: String, indentLevel: Int) => {
+ def join[T](caseClass: CaseClass[ShowPretty, T]): ShowPretty[T] = {
+ (t: T, sb: StringBuilder, indentWith: String, indentLevel: Int) =>
val nextIndent = indentLevel + 1
val lastIndex = caseClass.parameters.size - 1
- sb.append(caseClass.typeName.full).append("(\n")
+ void(sb.append(caseClass.typeInfo.full).append("(\n"))
caseClass.parameters.foreach { p =>
- sb.append(indentWith * nextIndent).append(p.label).append(" = ")
- p.typeclass.showPretty(p.dereference(t), sb, indentWith, nextIndent)
+ void(sb.append(indentWith * nextIndent).append(p.label).append(" = "))
+ void(p.typeclass.showPretty(p.deref(t), sb, indentWith, nextIndent))
if (p.index =!= lastIndex) {
sb.append(",")
}
sb.append("\n")
}
sb.append(indentWith * indentLevel).append(")")
- }
+ }
- def split[T](sealedTrait: SealedTrait[Typeclass, T]): Typeclass[T] =
+ def split[T](sealedTrait: SealedTrait[ShowPretty, T]): ShowPretty[T] =
(t: T, sb: StringBuilder, indentWith: String, indentLevel: Int) =>
- sealedTrait.split(t)(sub => sub.typeclass.showPretty(sub.cast(t), sb, indentWith, indentLevel))
-
- def semi[T]: Typeclass[T] = macro Magnolia.gen[T]
+ sealedTrait.choose(t)(sub => sub.typeclass.showPretty(sub.cast(t), sb, indentWith, indentLevel))
}
+@SuppressWarnings(Array("org.wartremover.warts.MutableDataStructures"))
trait ShowPrettyLowLevel { self: ShowPretty.type =>
- implicit def liftShow[T](implicit normalShow: Show[T]): ShowPretty[T] =
+ given liftShow[T](using normalShow: Show[T]): ShowPretty[T] =
(t: T, sb: StringBuilder, _: String, _: Int) => sb.append(normalShow.show(t))
}
diff --git a/modules/common/src/main/scala/io/branchtalk/shared/model/UUIDGenerator.scala b/modules/common/src/main/scala/io/branchtalk/shared/model/UUIDGenerator.scala
deleted file mode 100644
index bf742e75..00000000
--- a/modules/common/src/main/scala/io/branchtalk/shared/model/UUIDGenerator.scala
+++ /dev/null
@@ -1,22 +0,0 @@
-package io.branchtalk.shared.model
-
-import cats.effect.Sync
-import com.eatthepath.uuid.FastUUID
-import com.fasterxml.uuid.Generators
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.string.Uuid
-
-trait UUIDGenerator {
-
- def apply(string: String Refined Uuid): UUID
- def create[F[_]: Sync]: F[UUID]
- def parse[F[_]: Sync](string: String): F[UUID]
-}
-
-object UUIDGenerator {
- object FastUUIDGenerator extends UUIDGenerator {
- override def apply(string: Refined[String, Uuid]): UUID = FastUUID.parseUUID(string.value)
- override def create[F[_]: Sync]: F[UUID] = Sync[F].delay(Generators.timeBasedGenerator().generate())
- override def parse[F[_]: Sync](string: String): F[UUID] = Sync[F].delay(FastUUID.parseUUID(string))
- }
-}
diff --git a/modules/common/src/main/scala/io/branchtalk/shared/model/Updatable.scala b/modules/common/src/main/scala/io/branchtalk/shared/model/Updatable.scala
deleted file mode 100644
index e84a8a18..00000000
--- a/modules/common/src/main/scala/io/branchtalk/shared/model/Updatable.scala
+++ /dev/null
@@ -1,33 +0,0 @@
-package io.branchtalk.shared.model
-
-import cats.{ Applicative, Traverse }
-import io.branchtalk.ADT
-import io.scalaland.catnip.Semi
-
-// Express the intent that something should be updated or not better than Option.
-@Semi(ShowPretty, FastEq) sealed trait Updatable[+A] extends ADT {
-
- def fold[B](set: A => B, keep: => B): B = this match {
- case Updatable.Set(value) => set(value)
- case Updatable.Keep => keep
- }
-
- def toOption: Option[A] = this match {
- case Updatable.Set(value) => Some(value)
- case Updatable.Keep => None
- }
-}
-object Updatable {
- final case class Set[+A](value: A) extends Updatable[A]
- case object Keep extends Updatable[Nothing]
-
- private val applicative: Applicative[Updatable] = new Applicative[Updatable] {
- override def pure[A](a: A): Updatable[A] = Set(a)
- override def ap[A, B](ff: Updatable[A => B])(fa: Updatable[A]): Updatable[B] = (ff, fa) match {
- case (Set(f), Set(a)) => Set(f(a))
- case _ => Keep
- }
- }
- private val traverse: Traverse[Updatable] = cats.derived.semiauto.traverse[Updatable]
- implicit val appTraverse: ApplicativeTraverse[Updatable] = ApplicativeTraverse.semi(applicative, traverse)
-}
diff --git a/modules/common/src/main/scala/io/branchtalk/shared/model/avro.scala b/modules/common/src/main/scala/io/branchtalk/shared/model/avro.scala
new file mode 100644
index 00000000..296a5623
--- /dev/null
+++ b/modules/common/src/main/scala/io/branchtalk/shared/model/avro.scala
@@ -0,0 +1,174 @@
+package io.branchtalk.shared.model
+
+import java.io.{ ByteArrayInputStream, ByteArrayOutputStream }
+import java.net.URI
+import java.util
+import cats.{ Eq, Id, Show }
+import cats.data.{ Chain, NonEmptyChain, NonEmptyList, NonEmptyVector }
+import cats.effect.{ Resource, Sync, SyncIO }
+import com.sksamuel.avro4s.*
+import io.scalaland.chimney.partial
+import org.apache.avro.Schema
+import neotype.*
+
+import scala.collection.compat.Factory
+import scala.jdk.CollectionConverters.*
+import scala.util.{ Failure, Success }
+import scala.util.control.NoStackTrace
+import org.apache.avro.Schema
+
+import scala.language.implicitConversions
+
+// Missing helpers for serializations and deserialization with some API saner than byte array streams.
+enum DeserializationError extends Throwable, NoStackTrace derives FastEq, ShowPretty {
+ case DecodingError(badValue: String, error: Throwable)
+ case DecryptionError(badValue: String, errors: Map[String, String])
+}
+object DeserializationError {
+
+ private given Show[Throwable] = _.getMessage
+ private given Eq[Throwable] = _.getMessage === _.getMessage
+
+ given partial.AsResult[AvroSerialization.DeserializationResult] with {
+ def asResult[A](fa: AvroSerialization.DeserializationResult[A]): partial.Result[A] =
+ fa.fold[partial.Result[A]](partial.Result.fromErrorThrowable, partial.Result.fromValue)
+ }
+}
+
+object AvroSerialization {
+
+ private val logger = com.typesafe.scalalogging.Logger(getClass)
+
+ type DeserializationResult[+A] = Either[DeserializationError, A]
+
+ def serialize[F[_]: Sync, A: Encoder: SchemaFor](value: A): F[Array[Byte]] =
+ Resource.fromAutoCloseable(Sync[F].delay(new ByteArrayOutputStream())).use { baos =>
+ Sync[F].delay {
+ val aos = AvroOutputStream.json[A].to(baos).build()
+ aos.write(value)
+ aos.close()
+ aos.flush()
+ baos.toByteArray
+ }
+ }
+
+ @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf", "org.wartremover.warts.Throw"))
+ def deserialize[F[_]: Sync, A: Decoder: SchemaFor](arr: Array[Byte]): F[DeserializationResult[A]] =
+ Resource.fromAutoCloseable(Sync[F].delay(new ByteArrayInputStream(arr))).use { bais =>
+ Sync[F]
+ .delay {
+ AvroInputStream
+ .json[A]
+ .from(bais)
+ .build(SchemaFor[A].schema)
+ .asInstanceOf[AvroJsonInputStream[A]]
+ .singleEntity match {
+ case Success(value) =>
+ value.asRight[DeserializationError]
+ case Failure(error) =>
+ DeserializationError.DecodingError("Failed to extract Avro message", error).asLeft[A]
+ }
+ }
+ .handleError { (error: Throwable) =>
+ logger.error(s"Avro deserialization error for '${new String(arr, branchtalkCharset)}'", error)
+ DeserializationError.DecodingError(new String(arr, branchtalkCharset), error).asLeft[A]
+ }
+ }
+
+ def serializeUnsafe[A: Encoder: SchemaFor](value: A): Array[Byte] =
+ serialize[SyncIO, A](value).unsafeRunSync()
+
+ def deserializeUnsafe[A: Decoder: SchemaFor](arr: Array[Byte]): DeserializationResult[A] =
+ deserialize[SyncIO, A](arr).unsafeRunSync()
+}
+
+object AvroSupport {
+
+ // newtype - order of implicits is necessary (swapping them would break derivations, so we can't use typeclass syntax)
+
+ @SuppressWarnings(Array("org.wartremover.warts.Throw"))
+ given [A, B](using A: Newtype.WithType[B, A], B: Decoder[B]): Decoder[A] =
+ B.map[A](b => A.make(b).fold[A](str => throw Avro4sDecodingException(str, b), identity[A]))
+ given [A, B](using A: Newtype.WithType[B, A], B: Encoder[B]): Encoder[A] = B.contramap[A](A.unwrap)
+ given [A, B](using A: Newtype.WithType[B, A], B: SchemaFor[B]): SchemaFor[A] = B.forType[A]
+
+ // cats - copy pased because:
+ // Implementation restriction: package com.sksamuel.avro4s.cats is not a valid prefix for a wildcard export, as it is a package
+
+ import scala.jdk.CollectionConverters.*
+
+ given [T](using schemaFor: SchemaFor[T]): SchemaFor[NonEmptyList[T]] = SchemaFor(Schema.createArray(schemaFor.schema))
+ given [T](using schemaFor: SchemaFor[T]): SchemaFor[NonEmptyVector[T]] = SchemaFor(
+ Schema.createArray(schemaFor.schema)
+ )
+ given [T](using schemaFor: SchemaFor[T]): SchemaFor[NonEmptyChain[T]] = SchemaFor(
+ Schema.createArray(schemaFor.schema)
+ )
+
+ @SuppressWarnings(Array("org.wartremover.warts.Equals"))
+ given [T](using encoder: Encoder[T]): Encoder[NonEmptyList[T]] = (schema: Schema) => {
+ require(schema.getType == Schema.Type.ARRAY)
+ val encode = encoder.encode(schema)
+ (value: NonEmptyList[T]) => value.map(encode).toList.asJava
+ }
+
+ @SuppressWarnings(Array("org.wartremover.warts.Equals"))
+ given [T](using encoder: Encoder[T]): Encoder[NonEmptyVector[T]] = (schema: Schema) => {
+ require(schema.getType == Schema.Type.ARRAY)
+ val encode = encoder.encode(schema)
+ (value: NonEmptyVector[T]) => value.map(encode).toVector.asJava
+ }
+
+ @SuppressWarnings(Array("org.wartremover.warts.Equals"))
+ given [T](using encoder: Encoder[T]): Encoder[NonEmptyChain[T]] = (schema: Schema) => {
+ require(schema.getType == Schema.Type.ARRAY)
+ val encode = encoder.encode(schema)
+ (value: NonEmptyChain[T]) => value.map(encode).toNonEmptyList.toList.asJava
+ }
+
+ @SuppressWarnings(Array("org.wartremover.warts.Equals"))
+ given [T](using decoder: Decoder[T]): Decoder[NonEmptyList[T]] = (schema: Schema) => {
+ require(schema.getType == Schema.Type.ARRAY)
+ val decode = decoder.decode(schema)
+ (value: Any) =>
+ value match {
+ case array: Array[?] if array.nonEmpty => NonEmptyList.fromListUnsafe(array.toList.map(decode))
+ case list: java.util.Collection[?] if !list.isEmpty =>
+ NonEmptyList.fromListUnsafe(list.asScala.map(decode).toList)
+ case other => sys.error(s"Unsupported type $other")
+ }
+ }
+
+ @SuppressWarnings(Array("org.wartremover.warts.Equals"))
+ given [T](using decoder: Decoder[T]): Decoder[NonEmptyVector[T]] = (schema: Schema) => {
+ require(schema.getType == Schema.Type.ARRAY)
+ val decode = decoder.decode(schema)
+ (value: Any) =>
+ value match {
+ case array: Array[?] if array.nonEmpty => NonEmptyVector.fromVectorUnsafe(array.toVector.map(decode))
+ case list: java.util.Collection[?] if !list.isEmpty =>
+ NonEmptyVector.fromVectorUnsafe(list.asScala.map(decode).toVector)
+ case other => sys.error(s"Unsupported type $other") // A
+ }
+ }
+
+ @SuppressWarnings(Array("org.wartremover.warts.Equals", "org.wartremover.warts.OptionPartial"))
+ given [T](using decoder: Decoder[T]): Decoder[NonEmptyChain[T]] = (schema: Schema) => {
+ require(schema.getType == Schema.Type.ARRAY)
+ val decode = decoder.decode(schema)
+ (value: Any) =>
+ value match {
+ case array: Array[?] if array.nonEmpty => NonEmptyChain.fromChainUnsafe(Chain.fromSeq(array.toSeq).map(decode))
+ case list: java.util.Collection[?] if !list.isEmpty =>
+ NonEmptyChain.fromChainUnsafe(Chain.fromSeq(list.asScala.toSeq).map(decode))
+ case other => sys.error(s"Unsupported type $other")
+ }
+ }
+
+ // custom types
+
+ given Decoder[URI] = Decoder[String].map(URI.create)
+ @SuppressWarnings(Array("org.wartremover.warts.ToString")) // false warning - URI overrides toString
+ given Encoder[URI] = Encoder[String].contramap[URI](_.toString)
+ given SchemaFor[URI] = SchemaFor[String].forType[URI]
+}
diff --git a/modules/common/src/main/scala/io/branchtalk/shared/model/model.scala b/modules/common/src/main/scala/io/branchtalk/shared/model/model.scala
deleted file mode 100644
index 2712bd48..00000000
--- a/modules/common/src/main/scala/io/branchtalk/shared/model/model.scala
+++ /dev/null
@@ -1,158 +0,0 @@
-package io.branchtalk.shared
-
-import java.nio.charset.{ Charset, StandardCharsets }
-import java.time.{ OffsetDateTime, ZoneId }
-import java.time.format.DateTimeFormatter
-import java.util.regex.Pattern
-import java.util.{ Locale, UUID => jUUID }
-import cats.effect.{ Clock, Sync }
-import cats.{ Eq, Functor, Order, Show }
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.string.Uuid
-import io.estatico.newtype.macros.newtype
-import io.estatico.newtype.ops._
-import org.typelevel.log4cats.SelfAwareStructuredLogger
-import org.typelevel.log4cats.slf4j.Slf4jLogger
-
-import scala.annotation.unused
-import scala.reflect.runtime.universe.{ WeakTypeTag, weakTypeOf }
-
-package object model {
-
- implicit class ShowCompanionOps(@unused private val s: Show.type) extends AnyVal {
-
- // displays "wrapper.package.WrapperName(content)"
- def wrap[Wrapper: WeakTypeTag, Content: Show](f: Wrapper => Content): Show[Wrapper] = {
- @SuppressWarnings(Array("org.wartremover.warts.ToString")) val name = weakTypeOf[Wrapper].toString
- s => show"$name(${f(s)})"
- }
- }
-
- type Logger[F[_]] = SelfAwareStructuredLogger[F]
- val Logger = Slf4jLogger
-
- type UUID = jUUID
- object UUID {
-
- def apply(string: String Refined Uuid)(implicit uuidGenerator: UUIDGenerator): UUID =
- uuidGenerator(string)
- def create[F[_]: Sync](implicit uuidGenerator: UUIDGenerator): F[UUID] =
- uuidGenerator.create[F]
- def parse[F[_]: Sync](string: String)(implicit uuidGenerator: UUIDGenerator): F[UUID] =
- uuidGenerator.parse[F](string)
- }
-
- @newtype final case class ID[+Entity](uuid: UUID)
- object ID {
- def unapply[Entity](entity: ID[Entity]): Some[UUID] = Some(entity.uuid)
- def create[F[_]: Sync, Entity](implicit uuidGenerator: UUIDGenerator): F[ID[Entity]] =
- UUID.create[F].map(ID[Entity])
- def parse[F[_]: Sync, Entity](string: String)(implicit uuidGenerator: UUIDGenerator): F[ID[Entity]] =
- UUID.parse[F](string).map(ID[Entity])
-
- implicit def show[Entity]: Show[ID[Entity]] = Show.wrap(_.uuid)
- implicit def order[Entity]: Order[ID[Entity]] = Order[UUID].coerce[Order[ID[Entity]]]
- }
-
- @newtype final case class CreationTime(offsetDateTime: OffsetDateTime)
- object CreationTime {
- def unapply(creationTime: CreationTime): Some[OffsetDateTime] = Some(creationTime.offsetDateTime)
- def now[F[_]: Functor: Clock]: F[CreationTime] =
- Clock[F].realTimeInstant.map(OffsetDateTime.ofInstant(_, ZoneId.systemDefault())).map(CreationTime(_))
-
- implicit val show: Show[CreationTime] = Show.wrap(_.offsetDateTime.pipe(DateTimeFormatter.ISO_INSTANT.format))
- implicit val order: Order[CreationTime] =
- Order.by[CreationTime, OffsetDateTime](_.offsetDateTime)(Order.fromComparable)
- }
- @newtype final case class ModificationTime(offsetDateTime: OffsetDateTime)
- object ModificationTime {
- def unapply(modificationTime: ModificationTime): Some[OffsetDateTime] = Some(modificationTime.offsetDateTime)
- def now[F[_]: Functor: Clock]: F[ModificationTime] =
- Clock[F].realTimeInstant.map(OffsetDateTime.ofInstant(_, ZoneId.systemDefault())).map(ModificationTime(_))
-
- implicit val show: Show[ModificationTime] = Show.wrap(_.offsetDateTime.pipe(DateTimeFormatter.ISO_INSTANT.format))
- implicit val order: Order[ModificationTime] =
- Order.by[ModificationTime, OffsetDateTime](_.offsetDateTime)(Order.fromComparable)
- }
-
- @newtype final case class CreationScheduled[Entity](id: ID[Entity])
- object CreationScheduled {
- def unapply[Entity](creationScheduled: CreationScheduled[Entity]): Some[ID[Entity]] = Some(creationScheduled.id)
-
- implicit def show[Entity]: Show[CreationScheduled[Entity]] = Show.wrap(_.id)
- implicit def eq[Entity]: Eq[CreationScheduled[Entity]] = Eq[ID[Entity]].coerce
- }
- @newtype final case class UpdateScheduled[Entity](id: ID[Entity])
- object UpdateScheduled {
- def unapply[Entity](updateScheduled: UpdateScheduled[Entity]): Some[ID[Entity]] = Some(updateScheduled.id)
-
- implicit def show[Entity]: Show[UpdateScheduled[Entity]] = Show.wrap(_.id)
- implicit def eq[Entity]: Eq[UpdateScheduled[Entity]] = Eq[ID[Entity]].coerce
- }
- @newtype final case class DeletionScheduled[Entity](id: ID[Entity])
- object DeletionScheduled {
- def unapply[Entity](deletionScheduled: DeletionScheduled[Entity]): Some[ID[Entity]] = Some(deletionScheduled.id)
-
- implicit def show[Entity]: Show[DeletionScheduled[Entity]] = Show.wrap(_.id)
- implicit def eq[Entity]: Eq[DeletionScheduled[Entity]] = Eq[ID[Entity]].coerce
- }
- @newtype final case class RestoreScheduled[Entity](id: ID[Entity])
- object RestoreScheduled {
- def unapply[Entity](restoreScheduled: RestoreScheduled[Entity]): Some[ID[Entity]] = Some(restoreScheduled.id)
-
- implicit def show[Entity]: Show[RestoreScheduled[Entity]] = Show.wrap(_.id)
- implicit def eq[Entity]: Eq[RestoreScheduled[Entity]] = Eq[ID[Entity]].coerce
- }
-
- implicit class Untupled2[I1, I2, Out](private val f: ((I1, I2)) => Out) extends AnyVal {
- def untupled(i1: I1, i2: I2): Out = f.apply((i1, i2))
- }
- implicit class Untupled2A[I1, I2, Out](private val f: (I1, I2) => Out) extends AnyVal {
- def untupled(i1: I1, i2: I2): Out = f.apply(i1, i2)
- }
- implicit class Untupled3[I1, I2, I3, Out](private val f: ((I1, I2, I3)) => Out) extends AnyVal {
- def untupled(i1: I1, i2: I2, i3: I3): Out = f.apply((i1, i2, i3))
- }
- implicit class Untupled3A[I1, I2, I3, Out](private val f: (I1, (I2, I3)) => Out) extends AnyVal {
- def untupled(i1: I1, i2: I2, i3: I3): Out = f.apply(i1, (i2, i3))
- }
- implicit class Untupled4[I1, I2, I3, I4, Out](private val f: ((I1, I2, I3, I4)) => Out) extends AnyVal {
- def untupled(i1: I1, i2: I2, i3: I3, i4: I4): Out = f.apply((i1, i2, i3, i4))
- }
- implicit class Untupled4A[I1, I2, I3, I4, Out](private val f: (I1, (I2, I3, I4)) => Out) extends AnyVal {
- def untupled(i1: I1, i2: I2, i3: I3, i4: I4): Out = f.apply(i1, (i2, i3, i4))
- }
- implicit class Untupled5[I1, I2, I3, I4, I5, Out](private val f: ((I1, I2, I3, I4, I5)) => Out) extends AnyVal {
- def untupled(i1: I1, i2: I2, i3: I3, i4: I4, i5: I5): Out = f.apply((i1, i2, i3, i4, i5))
- }
- implicit class Untupled5A[I1, I2, I3, I4, I5, Out](private val f: (I1, (I2, I3, I4, I5)) => Out) extends AnyVal {
- def untupled(i1: I1, i2: I2, i3: I3, i4: I4, i5: I5): Out = f.apply(i1, (i2, i3, i4, i5))
- }
- implicit class Untupled6[I1, I2, I3, I4, I5, I6, Out](private val f: ((I1, I2, I3, I4, I5, I6)) => Out)
- extends AnyVal {
- def untupled(i1: I1, i2: I2, i3: I3, i4: I4, i5: I5, i6: I6): Out = f.apply((i1, i2, i3, i4, i5, i6))
- }
- implicit class Untupled6A[I1, I2, I3, I4, I5, I6, Out](private val f: (I1, (I2, I3, I4, I5, I6)) => Out)
- extends AnyVal {
- def untupled(i1: I1, i2: I2, i3: I3, i4: I4, i5: I5, i6: I6): Out = f.apply(i1, (i2, i3, i4, i5, i6))
- }
- implicit class Untupled7[I1, I2, I3, I4, I5, I6, I7, Out](private val f: ((I1, I2, I3, I4, I5, I6, I7)) => Out)
- extends AnyVal {
- def untupled(i1: I1, i2: I2, i3: I3, i4: I4, i5: I5, i6: I6, i7: I7): Out = f.apply((i1, i2, i3, i4, i5, i6, i7))
- }
- implicit class Untupled7A[I1, I2, I3, I4, I5, I6, I7, Out](private val f: (I1, (I2, I3, I4, I5, I6, I7)) => Out)
- extends AnyVal {
- def untupled(i1: I1, i2: I2, i3: I3, i4: I4, i5: I5, i6: I6, i7: I7): Out = f.apply(i1, (i2, i3, i4, i5, i6, i7))
- }
-
- val branchtalkCharset: Charset = StandardCharsets.UTF_8 // used in getBytes and new String
- val branchtalkLocale: Locale = Locale.ROOT // used in toLowerCase(branchtalkLocale) in Meta definitions
-
- private val basePattern: Pattern = Pattern.compile("([A-Z]+)([A-Z][a-z])")
- private val swapPattern: Pattern = Pattern.compile("([a-z\\d])([A-Z])")
- def discriminatorNameMapper(separator: String): String => String = in => {
- val simpleName = in.substring(in.lastIndexOf(separator) + separator.length)
- val partial = basePattern.matcher(simpleName).replaceAll("$1-$2")
- swapPattern.matcher(partial).replaceAll("$1-$2").toLowerCase(branchtalkLocale)
- }
-}
diff --git a/modules/common/src/main/scala/io/branchtalk/shared/model/scheduled.scala b/modules/common/src/main/scala/io/branchtalk/shared/model/scheduled.scala
new file mode 100644
index 00000000..4c696051
--- /dev/null
+++ b/modules/common/src/main/scala/io/branchtalk/shared/model/scheduled.scala
@@ -0,0 +1,40 @@
+package io.branchtalk.shared.model
+
+import cats.{ Eq, Show }
+import neotype.*
+
+type CreationScheduled[A] = CreationScheduled.Type[A]
+object CreationScheduled extends NewtypeF[ID] {
+
+ def unapply[Entity](creationScheduled: CreationScheduled[Entity]): Some[ID[Entity]] = Some(creationScheduled.unwrap)
+
+ given [Entity]: Show[CreationScheduled[Entity]] = unsafeMakeF[Show, Entity](Show[ID[Entity]])
+ given [Entity]: Eq[CreationScheduled[Entity]] = unsafeMakeF[Eq, Entity](Eq[ID[Entity]])
+}
+
+type UpdateScheduled[A] = UpdateScheduled.Type[A]
+object UpdateScheduled extends NewtypeF[ID] {
+
+ def unapply[Entity](updateScheduled: UpdateScheduled[Entity]): Some[ID[Entity]] = Some(updateScheduled.unwrap)
+
+ given [Entity]: Show[UpdateScheduled[Entity]] = unsafeMakeF[Show, Entity](Show[ID[Entity]])
+ given [Entity]: Eq[UpdateScheduled[Entity]] = unsafeMakeF[Eq, Entity](Eq[ID[Entity]])
+}
+
+type DeletionScheduled[A] = DeletionScheduled.Type[A]
+object DeletionScheduled extends NewtypeF[ID] {
+
+ def unapply[Entity](deletionScheduled: DeletionScheduled[Entity]): Some[ID[Entity]] = Some(deletionScheduled.unwrap)
+
+ given [Entity]: Show[DeletionScheduled[Entity]] = unsafeMakeF[Show, Entity](Show[ID[Entity]])
+ given [Entity]: Eq[DeletionScheduled[Entity]] = unsafeMakeF[Eq, Entity](Eq[ID[Entity]])
+}
+
+type RestoreScheduled[A] = RestoreScheduled.Type[A]
+object RestoreScheduled extends NewtypeF[ID] {
+
+ def unapply[Entity](restoreScheduled: RestoreScheduled[Entity]): Some[ID[Entity]] = Some(restoreScheduled.unwrap)
+
+ given [Entity]: Show[RestoreScheduled[Entity]] = unsafeMakeF[Show, Entity](Show[ID[Entity]])
+ given [Entity]: Eq[RestoreScheduled[Entity]] = unsafeMakeF[Eq, Entity](Eq[ID[Entity]])
+}
diff --git a/modules/common/src/main/scala/io/branchtalk/shared/model/strings.scala b/modules/common/src/main/scala/io/branchtalk/shared/model/strings.scala
new file mode 100644
index 00000000..87d14639
--- /dev/null
+++ b/modules/common/src/main/scala/io/branchtalk/shared/model/strings.scala
@@ -0,0 +1,22 @@
+package io.branchtalk.shared.model
+
+import java.nio.charset.{ Charset, StandardCharsets }
+import java.util.Locale
+import java.util.regex.Pattern
+
+val branchtalkCharset: Charset = StandardCharsets.UTF_8 // used in getBytes and new String
+val branchtalkLocale: Locale = Locale.ROOT // used in toLowerCase(branchtalkLocale) in Meta definitions
+
+private val basePattern: Pattern = Pattern.compile("([A-Z]+)([A-Z][a-z])")
+private val swapPattern: Pattern = Pattern.compile("([a-z\\d])([A-Z])")
+def discriminatorNameMapper(separator: String): String => String = in => {
+ val simpleName = in.substring(in.lastIndexOf(separator) + separator.length)
+ val partial = basePattern.matcher(simpleName).replaceAll("$1-$2")
+ swapPattern.matcher(partial).replaceAll("$1-$2").toLowerCase(branchtalkLocale)
+}
+// String => String has to be object rather than val, so that Jsoniter macro could find it
+object adtDiscriminatorNameMapper extends (String => String) {
+ private val impl = discriminatorNameMapper(".")
+
+ override def apply(name: String): String = impl(name)
+}
diff --git a/modules/common/src/main/scala/io/branchtalk/shared/model/times.scala b/modules/common/src/main/scala/io/branchtalk/shared/model/times.scala
new file mode 100644
index 00000000..ece0c98e
--- /dev/null
+++ b/modules/common/src/main/scala/io/branchtalk/shared/model/times.scala
@@ -0,0 +1,27 @@
+package io.branchtalk.shared.model
+
+import java.time.{ OffsetDateTime, ZoneId }
+import java.time.format.DateTimeFormatter
+import cats.{ Functor, Order, Show }
+import cats.effect.{ Clock, Sync }
+import neotype.*
+
+type CreationTime = CreationTime.Type
+object CreationTime extends Newtype[OffsetDateTime] {
+ def unapply(creationTime: CreationTime): Some[OffsetDateTime] = Some(creationTime.unwrap)
+ def now[F[_]: Functor: Clock]: F[CreationTime] =
+ Clock[F].realTimeInstant.map(OffsetDateTime.ofInstant(_, ZoneId.systemDefault())).map(unsafeMake)
+
+ given Show[CreationTime] = unsafeMakeF[Show](_.pipe(DateTimeFormatter.ISO_INSTANT.format))
+ given Order[CreationTime] = unsafeMakeF[Order](Order.fromComparable)
+}
+
+type ModificationTime = ModificationTime.Type
+object ModificationTime extends Newtype[OffsetDateTime] {
+ def unapply(ModificationTime: ModificationTime): Some[OffsetDateTime] = Some(ModificationTime.unwrap)
+ def now[F[_]: Functor: Clock]: F[ModificationTime] =
+ Clock[F].realTimeInstant.map(OffsetDateTime.ofInstant(_, ZoneId.systemDefault())).map(unsafeMake)
+
+ given Show[ModificationTime] = unsafeMakeF[Show](_.pipe(DateTimeFormatter.ISO_INSTANT.format))
+ given Order[ModificationTime] = unsafeMakeF[Order](Order.fromComparable)
+}
diff --git a/modules/common/src/main/scala/io/branchtalk/shared/model/untupled_ops.scala b/modules/common/src/main/scala/io/branchtalk/shared/model/untupled_ops.scala
new file mode 100644
index 00000000..18729380
--- /dev/null
+++ b/modules/common/src/main/scala/io/branchtalk/shared/model/untupled_ops.scala
@@ -0,0 +1,41 @@
+package io.branchtalk.shared.model
+
+extension [I1, I2, Out](f: ((I1, I2)) => Out) def untupled(i1: I1, i2: I2): Out = f.apply((i1, i2))
+
+extension [I1, I2, Out](f: (I1, I2) => Out) def untupled(i1: I1, i2: I2): Out = f.apply(i1, i2)
+
+extension [I1, I2, I3, Out](f: ((I1, I2, I3)) => Out) def untupled(i1: I1, i2: I2, i3: I3): Out = f.apply((i1, i2, i3))
+
+extension [I1, I2, I3, Out](f: (I1, (I2, I3)) => Out) def untupled(i1: I1, i2: I2, i3: I3): Out = f.apply(i1, (i2, i3))
+
+extension [I1, I2, I3, I4, Out](f: ((I1, I2, I3, I4)) => Out) {
+ def untupled(i1: I1, i2: I2, i3: I3, i4: I4): Out = f.apply((i1, i2, i3, i4))
+}
+
+extension [I1, I2, I3, I4, Out](f: (I1, (I2, I3, I4)) => Out) {
+ def untupled(i1: I1, i2: I2, i3: I3, i4: I4): Out = f.apply(i1, (i2, i3, i4))
+}
+
+extension [I1, I2, I3, I4, I5, Out](f: ((I1, I2, I3, I4, I5)) => Out) {
+ def untupled(i1: I1, i2: I2, i3: I3, i4: I4, i5: I5): Out = f.apply((i1, i2, i3, i4, i5))
+}
+
+extension [I1, I2, I3, I4, I5, Out](f: (I1, (I2, I3, I4, I5)) => Out) {
+ def untupled(i1: I1, i2: I2, i3: I3, i4: I4, i5: I5): Out = f.apply(i1, (i2, i3, i4, i5))
+}
+
+extension [I1, I2, I3, I4, I5, I6, Out](f: ((I1, I2, I3, I4, I5, I6)) => Out) {
+ def untupled(i1: I1, i2: I2, i3: I3, i4: I4, i5: I5, i6: I6): Out = f.apply((i1, i2, i3, i4, i5, i6))
+}
+
+extension [I1, I2, I3, I4, I5, I6, Out](f: (I1, (I2, I3, I4, I5, I6)) => Out) {
+ def untupled(i1: I1, i2: I2, i3: I3, i4: I4, i5: I5, i6: I6): Out = f.apply(i1, (i2, i3, i4, i5, i6))
+}
+
+extension [I1, I2, I3, I4, I5, I6, I7, Out](f: ((I1, I2, I3, I4, I5, I6, I7)) => Out) {
+ def untupled(i1: I1, i2: I2, i3: I3, i4: I4, i5: I5, i6: I6, i7: I7): Out = f.apply((i1, i2, i3, i4, i5, i6, i7))
+}
+
+extension [I1, I2, I3, I4, I5, I6, I7, Out](f: (I1, (I2, I3, I4, I5, I6, I7)) => Out) {
+ def untupled(i1: I1, i2: I2, i3: I3, i4: I4, i5: I5, i6: I6, i7: I7): Out = f.apply(i1, (i2, i3, i4, i5, i6, i7))
+}
diff --git a/modules/common/src/main/scala/io/branchtalk/shared/model/updatables.scala b/modules/common/src/main/scala/io/branchtalk/shared/model/updatables.scala
new file mode 100644
index 00000000..ef9adc66
--- /dev/null
+++ b/modules/common/src/main/scala/io/branchtalk/shared/model/updatables.scala
@@ -0,0 +1,66 @@
+package io.branchtalk.shared.model
+
+import cats.{ Applicative, Traverse }
+
+// Express the intent that something should be updated or not better than Option.
+enum Updatable[+A] derives ShowPretty, FastEq {
+ case Set(value: A)
+ case Keep
+
+ def fold[B](set: A => B, keep: => B): B = this match {
+ case Set(value) => set(value)
+ case Keep => keep
+ }
+
+ def toOption: Option[A] = this match {
+ case Set(value) => Some(value)
+ case Keep => None
+ }
+}
+object Updatable {
+
+ private val applicative: Applicative[Updatable] = new Applicative[Updatable] {
+ override def pure[A](a: A): Updatable[A] = Set(a)
+ override def ap[A, B](ff: Updatable[A => B])(fa: Updatable[A]): Updatable[B] = (ff, fa) match {
+ case (Set(f), Set(a)) => Set(f(a))
+ case _ => Keep
+ }
+ }
+ private val traverse: Traverse[Updatable] = cats.derived.semiauto.traverse[Updatable]
+ given ApplicativeTraverse[Updatable] = ApplicativeTraverse.derived(applicative, traverse)
+}
+
+// Express the intent that something should be updated, erased or kept better than Option[Either[Unit, *]].
+enum OptionUpdatable[+A] derives ShowPretty, FastEq {
+ case Set(value: A)
+ case Erase
+ case Keep
+
+ def fold[B](set: A => B, keep: => B, erase: => B): B = this match {
+ case Set(value) => set(value)
+ case Erase => keep
+ case Keep => erase
+ }
+
+ def toOptionEither: Option[Either[Unit, A]] = this match {
+ case Set(value) => Some(Right(value))
+ case Erase => Some(Left(()))
+ case Keep => None
+ }
+}
+object OptionUpdatable {
+
+ def setFromOption[A](option: Option[A]): OptionUpdatable[A] = option.fold[OptionUpdatable[A]](Erase)(Set(_))
+
+ private val applicative: Applicative[OptionUpdatable] = new Applicative[OptionUpdatable] {
+ override def pure[A](a: A): OptionUpdatable[A] = Set(a)
+ override def ap[A, B](ff: OptionUpdatable[A => B])(fa: OptionUpdatable[A]): OptionUpdatable[B] = (ff, fa) match {
+ case (Set(f), Set(a)) => Set(f(a))
+ case (Erase, _) => Erase
+ case (_, Erase) => Erase
+ case _ => Keep
+ }
+ }
+ private val traverse: Traverse[OptionUpdatable] = cats.derived.semiauto.traverse[OptionUpdatable]
+ given ApplicativeTraverse[OptionUpdatable] = ApplicativeTraverse.derived(applicative, traverse)
+}
diff --git a/modules/common/src/main/scala/io/branchtalk/shared/model/uuids.scala b/modules/common/src/main/scala/io/branchtalk/shared/model/uuids.scala
new file mode 100644
index 00000000..7d5052db
--- /dev/null
+++ b/modules/common/src/main/scala/io/branchtalk/shared/model/uuids.scala
@@ -0,0 +1,23 @@
+package io.branchtalk.shared.model
+
+import cats.effect.Sync
+import com.eatthepath.uuid.FastUUID
+import com.fasterxml.uuid.Generators
+
+type UUID = java.util.UUID
+object UUID {
+
+ val empty: UUID = java.util.UUID.fromString("00000000-0000-0000-0000-000000000000")
+
+ def create[F[_]: Sync](using generator: Generator): F[UUID] = generator.create[F]
+ def parse[F[_]: Sync](string: String)(using generator: Generator): F[UUID] = generator.parse[F](string)
+
+ trait Generator {
+ def create[F[_]: Sync]: F[UUID]
+ def parse[F[_]: Sync](string: String): F[UUID]
+ }
+ object FastGenerator extends Generator {
+ override def create[F[_]: Sync]: F[UUID] = Sync[F].delay(Generators.timeBasedGenerator().generate())
+ override def parse[F[_]: Sync](string: String): F[UUID] = Sync[F].delay(FastUUID.parseUUID(string))
+ }
+}
diff --git a/modules/common/src/main/scala/io/branchtalk/shared/model/void.scala b/modules/common/src/main/scala/io/branchtalk/shared/model/void.scala
new file mode 100644
index 00000000..e87e0310
--- /dev/null
+++ b/modules/common/src/main/scala/io/branchtalk/shared/model/void.scala
@@ -0,0 +1,8 @@
+package io.branchtalk.shared.model
+
+import scala.annotation.unused
+
+/** Explicitly mark value as unused, because it's e.g. returned from a mutable operation, and we care only about
+ * side-effect.
+ */
+inline def void[A](@unused value: A): Unit = ()
diff --git a/modules/common/src/main/scala/neotype/newtypes.scala b/modules/common/src/main/scala/neotype/newtypes.scala
new file mode 100644
index 00000000..1358a4fd
--- /dev/null
+++ b/modules/common/src/main/scala/neotype/newtypes.scala
@@ -0,0 +1,56 @@
+package neotype
+
+import cats.data.NonEmptyList
+import cats.effect.Sync
+
+// TODO: PR to upstream
+abstract class NewtypeT[A] { self =>
+ opaque type Type[B] = A
+
+ def validate(input: A): Boolean | String = true
+
+ inline def apply[B](inline input: A): Type[B] = instance[B].apply(input)
+ inline def applyAll[B](inline values: A*): List[Type[B]] = instance[B].applyAll(values*)
+
+ final def make[B](input: A): Either[String, Type[B]] = validate(input) match {
+ case true => Right(input)
+ case false => Left("Validation Failed")
+ case message: String => Left(message)
+ }
+
+ inline def unwrap[B](inline input: Type[B]): A = input
+ inline def unsafeMake[B](inline input: A): Type[B] = input
+ inline def unsafeMakeF[F[_], B](inline input: F[A]): F[Type[B]] = input
+
+ private val impl = new Newtype[A] {
+ override inline def validate(input: A): Boolean | String = self.validate(input)
+ }
+ @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
+ given instance[B]: Newtype.WithType[A, Type[B]] = impl.asInstanceOf[Newtype.WithType[A, Type[B]]]
+}
+
+// TODO: PR to upstream
+abstract class NewtypeF[F[_]] { self =>
+ opaque type Type[A] = F[A]
+
+ def validate[A](input: F[A]): Boolean | String = true
+
+ inline def apply[A](inline input: F[A]): Type[A] = instance[A].apply(input)
+ inline def applyAll[A](inline values: F[A]*): List[Type[A]] = instance[A].applyAll(values*)
+
+ final def make[A](input: F[A]): Either[String, Type[A]] = validate(input) match {
+ case true => Right(input)
+ case false => Left("Validation Failed")
+ case message: String => Left(message)
+ }
+
+ inline def unwrap[A](inline input: Type[A]): F[A] = input
+ inline def unsafeMake[A](inline input: F[A]): Type[A] = input
+ inline def unsafeMakeF[G[_], A](inline input: G[F[A]]): G[Type[A]] = input
+
+ private val impl = new Newtype[F[Any]] {
+ override inline def validate(input: F[Any]): Boolean | String = self.validate(input)
+ }
+ @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
+ given instance[A]: Newtype.WithType[F[A], Type[A]] = impl.asInstanceOf[Newtype.WithType[F[A], Type[A]]]
+}
diff --git a/modules/common/src/test/scala/com/devskiller/jfairy/makeFairy.scala b/modules/common/src/test/scala/com/devskiller/jfairy/makeFairy.scala
new file mode 100644
index 00000000..1e115006
--- /dev/null
+++ b/modules/common/src/test/scala/com/devskiller/jfairy/makeFairy.scala
@@ -0,0 +1,136 @@
+package com.devskiller.jfairy
+
+import com.devskiller.jfairy.data.*
+import com.devskiller.jfairy.producer
+import com.devskiller.jfairy.producer.*
+import com.devskiller.jfairy.producer.company.*
+import com.devskiller.jfairy.producer.net.*
+import com.devskiller.jfairy.producer.payment.*
+import com.devskiller.jfairy.producer.person.*
+import com.devskiller.jfairy.producer.text.*
+
+// workaround for default package
+def makeFairy(localeString: String): Fairy = {
+
+ // these psychopats used Guice for building this, and now I'm getting:
+ // com.google.inject.CreationException: Unable to create injector
+ val randomGenerator: RandomGenerator = RandomGenerator()
+ val baseProducer: BaseProducer = BaseProducer(randomGenerator)
+ val dataMaster: DataMaster = MapBasedDataMaster(baseProducer).tap { dm =>
+ dm.readResources("jfairy.yml")
+ dm.readResources("jfairy_" + localeString + ".yml")
+ }
+ val textProducer: TextProducer = makeTextProducer(baseProducer, dataMaster)
+ val timeProvider: TimeProvider = TimeProvider()
+ val dateProducer: DateProducer = DateProducer(baseProducer, timeProvider)
+ val networkProducer: NetworkProducer = makeNetworkProducer(baseProducer)
+ val creditCardProvider: CreditCardProvider = CreditCardProvider(dataMaster, baseProducer, dateProducer)
+ val (nationalIdentificationNumberFactory: NationalIdentificationNumberFactory,
+ nationalIdentityCardNumberProvider: NationalIdentityCardNumberProvider,
+ vatIdentificationNumberProvider: VATIdentificationNumberProvider,
+ addressProvider: AddressProvider,
+ passportNumberProvider: PassportNumberProvider
+ ) = localeString match {
+ case "de" =>
+ (
+ producer.person.locale.NoNationalIdentificationNumberFactory(baseProducer, dateProducer),
+ producer.person.locale.de.DeNationalIdentityCardNumberProvider(baseProducer),
+ producer.company.locale.de.DeVATIdentificationNumberProvider(),
+ producer.person.locale.de.DeAddressProvider(dataMaster, baseProducer),
+ producer.person.locale.de.DePassportNumberProvider(baseProducer)
+ )
+ case "es" =>
+ (
+ producer.person.locale.NoNationalIdentificationNumberFactory(baseProducer, dateProducer),
+ producer.person.locale.es.EsNationalIdentityCardNumberProvider(),
+ producer.company.locale.es.EsVATIdentificationNumberProvider(),
+ producer.person.locale.es.EsAddressProvider(dataMaster, baseProducer),
+ producer.person.locale.es.EsPassportNumberProvider()
+ )
+ /*
+ case "fr" =>
+ (
+ producer.person.locale.NoNationalIdentificationNumberFactory(baseProducer, dateProducer),
+ producer.person.locale.fr.FrNationalIdentityCardNumberProvider(baseProducer),
+ producer.company.locale.fr.FrVATIdentificationNumberProvider(baseProducer),
+ producer.person.locale.fr.FrAddressProvider(dataMaster, baseProducer),
+ producer.person.locale.fr.FrPassportNumberProvider()
+ )
+ */
+ case "ka" =>
+ (
+ producer.person.locale.NoNationalIdentificationNumberFactory(baseProducer, dateProducer),
+ producer.person.locale.ka.KaNationalIdentityCardNumberProvider(baseProducer),
+ producer.company.locale.ka.KaVATIdentificationNumberProvider(baseProducer),
+ producer.person.locale.ka.KaAddressProvider(dataMaster, baseProducer),
+ producer.person.locale.ka.KaPassportNumberProvider(baseProducer)
+ )
+ case "pl" =>
+ (
+ producer.person.locale.NoNationalIdentificationNumberFactory(baseProducer, dateProducer),
+ producer.person.locale.pl.PlNationalIdentityCardNumberProvider(dateProducer, baseProducer),
+ producer.company.locale.pl.PlVATIdentificationNumberProvider(baseProducer),
+ producer.person.locale.pl.PlAddressProvider(dataMaster, baseProducer),
+ producer.person.locale.pl.PlPassportNumberProvider()
+ )
+ case "sv" =>
+ val num = producer.person.locale.NoNationalIdentificationNumberFactory(baseProducer, dateProducer)
+ (
+ num,
+ producer.person.locale.sv.SvNationalIdentityCardNumberProvider(dateProducer, baseProducer),
+ producer.company.locale.sv.SvVATIdentificationNumberProvider(baseProducer, dateProducer, num),
+ producer.person.locale.sv.SvAddressProvider(dataMaster, baseProducer),
+ producer.person.locale.sv.SvPassportNumberProvider()
+ )
+ case "zh" =>
+ (
+ producer.person.locale.NoNationalIdentificationNumberFactory(baseProducer, dateProducer),
+ producer.person.locale.zh.ZhNationalIdentityCardNumberProvider(baseProducer),
+ producer.company.locale.zh.ZhVATIdentificationNumberProvider(),
+ producer.person.locale.zh.ZhAddressProvider(dataMaster, baseProducer),
+ producer.person.locale.zh.ZhPassportNumberProvider()
+ )
+ // en
+ case _ =>
+ (
+ producer.person.locale.NoNationalIdentificationNumberFactory(baseProducer, dateProducer),
+ producer.person.locale.en.EnNationalIdentityCardNumberProvider(baseProducer),
+ producer.company.locale.en.EnVATIdentificationNumberProvider(baseProducer),
+ producer.person.locale.en.EnAddressProvider(dataMaster, baseProducer),
+ producer.person.locale.en.EnPassportNumberProvider()
+ )
+ }
+ val companyFactory: CompanyFactory = new CompanyFactory {
+ override def produceCompany(companyProperties: CompanyProperties.CompanyProperty*): CompanyProvider =
+ DefaultCompanyProvider(baseProducer, dataMaster, vatIdentificationNumberProvider, companyProperties: _*)
+ }
+ val personFactory: PersonFactory = new PersonFactory {
+ override def producePersonProvider(personProperties: PersonProperties.PersonProperty*): PersonProvider =
+ DefaultPersonProvider(
+ dataMaster,
+ dateProducer,
+ baseProducer,
+ nationalIdentificationNumberFactory,
+ nationalIdentityCardNumberProvider,
+ addressProvider,
+ companyFactory,
+ passportNumberProvider,
+ timeProvider,
+ personProperties: _*
+ )
+ }
+ val ibanFactory: IBANFactory = new IBANFactory {
+ override def produceIBANProvider(properties: IBANProperties.Property*): IBANProvider =
+ DefaultIBANProvider(baseProducer, dataMaster, properties: _*)
+ }
+
+ Fairy(textProducer,
+ personFactory,
+ networkProducer,
+ baseProducer,
+ dateProducer,
+ creditCardProvider,
+ companyFactory,
+ ibanFactory
+ )
+}
diff --git a/modules/common/src/test/scala/com/devskiller/jfairy/producer/net/net_producers.scala b/modules/common/src/test/scala/com/devskiller/jfairy/producer/net/net_producers.scala
new file mode 100644
index 00000000..623240b0
--- /dev/null
+++ b/modules/common/src/test/scala/com/devskiller/jfairy/producer/net/net_producers.scala
@@ -0,0 +1,7 @@
+package com.devskiller.jfairy.producer.net
+
+import com.devskiller.jfairy.producer.*
+
+// workaround for default package
+def makeNetworkProducer(baseProducer: BaseProducer): NetworkProducer =
+ NetworkProducer(IPNumberProducer(baseProducer))
diff --git a/modules/common/src/test/scala/com/devskiller/jfairy/producer/text/text_producers.scala b/modules/common/src/test/scala/com/devskiller/jfairy/producer/text/text_producers.scala
new file mode 100644
index 00000000..196484c9
--- /dev/null
+++ b/modules/common/src/test/scala/com/devskiller/jfairy/producer/text/text_producers.scala
@@ -0,0 +1,9 @@
+package com.devskiller.jfairy.producer.text
+
+import com.devskiller.jfairy.data.*
+import com.devskiller.jfairy.producer.*
+import com.devskiller.jfairy.producer.text.*
+
+// workaround for default package
+def makeTextProducer(baseProducer: BaseProducer, dataMaster: DataMaster): TextProducer =
+ TextProducer(TextProducerInternal(dataMaster, baseProducer), baseProducer)
diff --git a/modules/common/src/test/scala/io/branchtalk/shared/Fixtures.scala b/modules/common/src/test/scala/io/branchtalk/shared/Fixtures.scala
index 75c2faab..db75016d 100644
--- a/modules/common/src/test/scala/io/branchtalk/shared/Fixtures.scala
+++ b/modules/common/src/test/scala/io/branchtalk/shared/Fixtures.scala
@@ -1,17 +1,19 @@
package io.branchtalk.shared
import cats.effect.IO
-import com.devskiller.jfairy.Fairy
-import com.devskiller.jfairy.producer.{ BaseProducer, DateProducer }
-import com.devskiller.jfairy.producer.company.{ Company, CompanyProperties }
-import com.devskiller.jfairy.producer.net.NetworkProducer
-import com.devskiller.jfairy.producer.payment.CreditCard
-import com.devskiller.jfairy.producer.person.{ Person, PersonProperties }
-import com.devskiller.jfairy.producer.text.TextProducer
+import com.devskiller.jfairy.*
+import com.devskiller.jfairy.data.*
+import com.devskiller.jfairy.producer
+import com.devskiller.jfairy.producer.*
+import com.devskiller.jfairy.producer.company.*
+import com.devskiller.jfairy.producer.net.*
+import com.devskiller.jfairy.producer.payment.*
+import com.devskiller.jfairy.producer.person.*
+import com.devskiller.jfairy.producer.text.*
object Fixtures {
- private val fairy = Fairy.create().pure[IO]
+ private val fairy = makeFairy("en").pure[IO]
val baseProducer: IO[BaseProducer] = fairy.map(_.baseProducer)
def company(companyProperties: CompanyProperties.CompanyProperty*): IO[Company] =
diff --git a/modules/common/src/test/scala/io/branchtalk/shared/model/TestUUIDGenerator.scala b/modules/common/src/test/scala/io/branchtalk/shared/model/TestUUIDGenerator.scala
index f4ced76a..0d216806 100644
--- a/modules/common/src/test/scala/io/branchtalk/shared/model/TestUUIDGenerator.scala
+++ b/modules/common/src/test/scala/io/branchtalk/shared/model/TestUUIDGenerator.scala
@@ -1,12 +1,10 @@
package io.branchtalk.shared.model
import cats.effect.Sync
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.string.Uuid
import scala.collection.mutable
-class TestUUIDGenerator extends UUIDGenerator {
+class TestUUIDGenerator extends UUID.Generator {
private val queue = mutable.Queue.empty[UUID]
@@ -14,22 +12,16 @@ class TestUUIDGenerator extends UUIDGenerator {
queue.enqueue(uuid)
()
}
- def stubNext(string: String Refined Uuid): Unit = synchronized {
- queue.enqueue(apply(string))
- ()
- }
def clean(): Unit = synchronized {
queue.dequeueAll(_ => true)
()
}
- override def apply(string: String Refined Uuid): UUID = UUIDGenerator.FastUUIDGenerator(string)
-
override def create[F[_]: Sync]: F[UUID] = synchronized {
- if (queue.isEmpty) UUIDGenerator.FastUUIDGenerator.create[F]
+ if (queue.isEmpty) UUID.FastGenerator.create[F]
else queue.dequeue().pure[F]
}
- override def parse[F[_]: Sync](string: String): F[UUID] = UUIDGenerator.FastUUIDGenerator.parse[F](string)
+ override def parse[F[_]: Sync](string: String): F[UUID] = UUID.FastGenerator.parse[F](string)
}
diff --git a/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/ChannelAPIs.scala b/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/ChannelAPIs.scala
index a362fbca..de68e0c3 100644
--- a/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/ChannelAPIs.scala
+++ b/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/ChannelAPIs.scala
@@ -1,9 +1,9 @@
package io.branchtalk.discussions.api
-import io.branchtalk.api._
-import io.branchtalk.api.AuthenticationSupport._
-import io.branchtalk.api.TapirSupport._
-import io.branchtalk.discussions.api.ChannelModels._
+import io.branchtalk.api.*
+import io.branchtalk.api.AuthenticationSupport.{ *, given }
+import io.branchtalk.api.TapirSupport.{ *, given }
+import io.branchtalk.discussions.api.ChannelModels.*
import io.branchtalk.discussions.model.Channel
import io.branchtalk.shared.model.{ ID, OptionUpdatable, Updatable }
import sttp.model.StatusCode
@@ -21,7 +21,7 @@ object ChannelAPIs {
val paginate: AuthedEndpoint[
Option[Authentication],
- (Option[PaginationOffset], Option[PaginationLimit]),
+ (Option[Pagination.Offset], Option[Pagination.Limit]),
ChannelError,
Pagination[APIChannel],
Any
@@ -33,8 +33,8 @@ object ChannelAPIs {
.get
.securityIn(optAuthHeader)
.in(prefix)
- .in(query[Option[PaginationOffset]]("offset"))
- .in(query[Option[PaginationLimit]]("limit"))
+ .in(query[Option[Pagination.Offset]]("offset"))
+ .in(query[Option[Pagination.Limit]]("limit"))
.out(jsonBody[Pagination[APIChannel]])
.errorOut(errorMapping)
.notRequiringPermissions
@@ -84,7 +84,7 @@ object ChannelAPIs {
List(
EndpointIO.Example.of(
UpdateChannelRequest(
- newUrlName = Updatable.Set(Channel.UrlName("example")),
+ newUrlName = Updatable.Set(Channel.UrlName.unsafeMake("example")), // should not be unsafeMake :(
newName = Updatable.Set(Channel.Name("example")),
newDescription = OptionUpdatable.Set(Channel.Description("example"))
),
@@ -115,7 +115,7 @@ object ChannelAPIs {
.out(jsonBody[UpdateChannelResponse])
.errorOut(errorMapping)
.requiringPermissions { case (channelID, _) =>
- RequiredPermissions.anyOf(Permission.IsOwner, Permission.ModerateChannel(ChannelID(channelID.uuid)))
+ RequiredPermissions.anyOf(Permission.IsOwner, Permission.ModerateChannel(ChannelID.unsafeMake(channelID.unwrap)))
}
val delete: AuthedEndpoint[Authentication, ID[Channel], ChannelError, DeleteChannelResponse, Any] = endpoint
@@ -129,7 +129,7 @@ object ChannelAPIs {
.out(jsonBody[DeleteChannelResponse])
.errorOut(errorMapping)
.requiringPermissions(channelID =>
- RequiredPermissions.anyOf(Permission.IsOwner, Permission.ModerateChannel(ChannelID(channelID.uuid)))
+ RequiredPermissions.anyOf(Permission.IsOwner, Permission.ModerateChannel(ChannelID.unsafeMake(channelID.unwrap)))
)
val restore: AuthedEndpoint[Authentication, ID[Channel], ChannelError, RestoreChannelResponse, Any] = endpoint
@@ -143,6 +143,6 @@ object ChannelAPIs {
.out(jsonBody[RestoreChannelResponse])
.errorOut(errorMapping)
.requiringPermissions(channelID =>
- RequiredPermissions.anyOf(Permission.IsOwner, Permission.ModerateChannel(ChannelID(channelID.uuid)))
+ RequiredPermissions.anyOf(Permission.IsOwner, Permission.ModerateChannel(ChannelID.unsafeMake(channelID.unwrap)))
)
}
diff --git a/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/ChannelModels.scala b/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/ChannelModels.scala
index c241c0dc..4f94bcab 100644
--- a/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/ChannelModels.scala
+++ b/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/ChannelModels.scala
@@ -1,78 +1,61 @@
package io.branchtalk.discussions.api
import cats.data.NonEmptyList
-import com.github.plokhotnyuk.jsoniter_scala.macros._
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.collection.NonEmpty
-import eu.timepit.refined.string.MatchesRegex
-import eu.timepit.refined.types.string.NonEmptyString
-import io.branchtalk.ADT
-import io.branchtalk.api.JsoniterSupport._
-import io.branchtalk.api.TapirSupport._
+import io.branchtalk.api.JsoniterSupport.{ *, given }
+import io.branchtalk.api.TapirSupport.{ *, given }
import io.branchtalk.discussions.model.Channel
import io.branchtalk.shared.model.{ ID, OptionUpdatable, Updatable }
-import io.scalaland.catnip.Semi
-import io.scalaland.chimney.dsl._
+import io.scalaland.chimney.dsl.*
-@SuppressWarnings(Array("org.wartremover.warts.All")) // for macros
object ChannelModels {
// properties codecs
+ given JsCodec[Channel.UrlName] = newtypeCodec
+ given JsCodec[Channel.Name] = newtypeCodec
+ given JsCodec[Channel.Description] = newtypeCodec
- implicit val channelUrlNameCodec: JsCodec[Channel.UrlName] =
- summonCodec[String](JsonCodecMaker.make).refine[MatchesRegex["[A-Za-z0-9_-]+"]].asNewtype[Channel.UrlName]
- implicit val channelNameCodec: JsCodec[Channel.Name] =
- summonCodec[String](JsonCodecMaker.make).refine[NonEmpty].asNewtype[Channel.Name]
- implicit val channelDescriptionCodec: JsCodec[Channel.Description] =
- summonCodec[String](JsonCodecMaker.make).refine[NonEmpty].asNewtype[Channel.Description]
-
- // properties schemas
- implicit val channelUrlNameSchema: JsSchema[Channel.UrlName] =
- summonSchema[String Refined MatchesRegex["[A-Za-z0-9_-]+"]].asNewtype[Channel.UrlName]
- implicit val channelNameSchema: JsSchema[Channel.Name] =
- summonSchema[NonEmptyString].asNewtype[Channel.Name]
- implicit val channelDescriptionSchema: JsSchema[Channel.Description] =
- summonSchema[NonEmptyString].asNewtype[Channel.Description]
-
- @Semi(JsCodec, JsSchema) sealed trait ChannelError extends ADT
+ sealed trait ChannelError derives DefaultJsCodec, JsSchema
object ChannelError {
- @Semi(JsCodec, JsSchema) final case class BadCredentials(msg: String) extends ChannelError
- @Semi(JsCodec, JsSchema) final case class NoPermission(msg: String) extends ChannelError
- @Semi(JsCodec, JsSchema) final case class NotFound(msg: String) extends ChannelError
- @Semi(JsCodec, JsSchema) final case class ValidationFailed(error: NonEmptyList[String]) extends ChannelError
+ final case class BadCredentials(msg: String) extends ChannelError derives DefaultJsCodec, JsSchema
+ final case class NoPermission(msg: String) extends ChannelError derives DefaultJsCodec, JsSchema
+ final case class NotFound(msg: String) extends ChannelError derives DefaultJsCodec, JsSchema
+ final case class ValidationFailed(error: NonEmptyList[String]) extends ChannelError derives DefaultJsCodec, JsSchema
}
- @Semi(JsCodec, JsSchema) final case class APIChannel(
+ final case class APIChannel(
id: ID[Channel],
urlName: Channel.UrlName,
name: Channel.Name,
description: Option[Channel.Description]
- )
+ ) derives DefaultJsCodec,
+ JsSchema
object APIChannel {
def fromDomain(channel: Channel): APIChannel =
channel.data.into[APIChannel].withFieldConst(_.id, channel.id).transform
}
- @Semi(JsCodec, JsSchema) final case class CreateChannelRequest(
+ final case class CreateChannelRequest(
urlName: Channel.UrlName,
name: Channel.Name,
description: Option[Channel.Description]
- )
+ ) derives DefaultJsCodec,
+ JsSchema
- @Semi(JsCodec, JsSchema) final case class CreateChannelResponse(id: ID[Channel])
+ final case class CreateChannelResponse(id: ID[Channel]) derives DefaultJsCodec, JsSchema
// TODO: unify behavior (Channel sets UrlName while Post generates it)
- @Semi(JsCodec, JsSchema) final case class UpdateChannelRequest(
+ final case class UpdateChannelRequest(
newUrlName: Updatable[Channel.UrlName],
newName: Updatable[Channel.Name],
newDescription: OptionUpdatable[Channel.Description]
- )
+ ) derives DefaultJsCodec,
+ JsSchema
- @Semi(JsCodec, JsSchema) final case class UpdateChannelResponse(id: ID[Channel])
+ final case class UpdateChannelResponse(id: ID[Channel]) derives DefaultJsCodec, JsSchema
- @Semi(JsCodec, JsSchema) final case class DeleteChannelResponse(id: ID[Channel])
+ final case class DeleteChannelResponse(id: ID[Channel]) derives DefaultJsCodec, JsSchema
- @Semi(JsCodec, JsSchema) final case class RestoreChannelResponse(id: ID[Channel])
+ final case class RestoreChannelResponse(id: ID[Channel]) derives DefaultJsCodec, JsSchema
}
diff --git a/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/CommentAPIs.scala b/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/CommentAPIs.scala
index 65f1794a..ef7c6453 100644
--- a/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/CommentAPIs.scala
+++ b/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/CommentAPIs.scala
@@ -1,9 +1,9 @@
package io.branchtalk.discussions.api
-import io.branchtalk.api._
-import io.branchtalk.api.AuthenticationSupport._
-import io.branchtalk.api.TapirSupport._
-import io.branchtalk.discussions.api.CommentModels._
+import io.branchtalk.api.*
+import io.branchtalk.api.AuthenticationSupport.{ *, given }
+import io.branchtalk.api.TapirSupport.{ *, given }
+import io.branchtalk.discussions.api.CommentModels.*
import io.branchtalk.discussions.model.{ Channel, Comment, Post }
import io.branchtalk.shared.model.{ ID, Updatable }
import sttp.model.StatusCode
@@ -23,7 +23,7 @@ object CommentAPIs {
val newest: AuthedEndpoint[
Option[Authentication],
- (ID[Channel], ID[Post], Option[PaginationOffset], Option[PaginationLimit], Option[ID[Comment]]),
+ (ID[Channel], ID[Post], Option[Pagination.Offset], Option[Pagination.Limit], Option[ID[Comment]]),
CommentError,
Pagination[APIComment],
Any
@@ -35,8 +35,8 @@ object CommentAPIs {
.get
.securityIn(optAuthHeader)
.in(prefix / "newest")
- .in(query[Option[PaginationOffset]]("offset"))
- .in(query[Option[PaginationLimit]]("limit"))
+ .in(query[Option[Pagination.Offset]]("offset"))
+ .in(query[Option[Pagination.Limit]]("limit"))
.in(query[Option[ID[Comment]]]("reply-to"))
.out(jsonBody[Pagination[APIComment]])
.errorOut(errorMapping)
@@ -155,7 +155,7 @@ object CommentAPIs {
.out(jsonBody[UpdateCommentResponse])
.errorOut(errorMapping)
.requiringPermissions { case (channelID, _, _, _) =>
- RequiredPermissions.anyOf(Permission.IsOwner, Permission.ModerateChannel(ChannelID(channelID.uuid)))
+ RequiredPermissions.anyOf(Permission.IsOwner, Permission.ModerateChannel(ChannelID(channelID.unwrap)))
}
val delete: AuthedEndpoint[
@@ -175,7 +175,7 @@ object CommentAPIs {
.out(jsonBody[DeleteCommentResponse])
.errorOut(errorMapping)
.requiringPermissions { case (channelID, _, _) =>
- RequiredPermissions.anyOf(Permission.IsOwner, Permission.ModerateChannel(ChannelID(channelID.uuid)))
+ RequiredPermissions.anyOf(Permission.IsOwner, Permission.ModerateChannel(ChannelID(channelID.unwrap)))
}
val restore: AuthedEndpoint[
@@ -195,7 +195,7 @@ object CommentAPIs {
.out(jsonBody[RestoreCommentResponse])
.errorOut(errorMapping)
.requiringPermissions { case (channelID, _, _) =>
- RequiredPermissions.anyOf(Permission.IsOwner, Permission.ModerateChannel(ChannelID(channelID.uuid)))
+ RequiredPermissions.anyOf(Permission.IsOwner, Permission.ModerateChannel(ChannelID(channelID.unwrap)))
}
val upvote: AuthedEndpoint[
diff --git a/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/CommentModels.scala b/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/CommentModels.scala
index b233e82a..bbe552ba 100644
--- a/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/CommentModels.scala
+++ b/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/CommentModels.scala
@@ -1,42 +1,28 @@
package io.branchtalk.discussions.api
import cats.data.NonEmptyList
-import com.github.plokhotnyuk.jsoniter_scala.macros._
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.numeric.NonNegative
-import io.branchtalk.ADT
-import io.branchtalk.api.JsoniterSupport._
-import io.branchtalk.api.TapirSupport._
+import io.branchtalk.api.JsoniterSupport.{ *, given }
+import io.branchtalk.api.TapirSupport.{ *, given }
import io.branchtalk.discussions.model.{ Channel, Comment, Post, User }
import io.branchtalk.shared.model.{ ID, Updatable }
-import io.scalaland.catnip.Semi
-import io.scalaland.chimney.dsl._
+import io.scalaland.chimney.dsl.*
-@SuppressWarnings(Array("org.wartremover.warts.All")) // for macros
object CommentModels {
// properties codecs
- implicit val commentContentCodec: JsCodec[Comment.Content] =
- summonCodec[String](JsonCodecMaker.make).asNewtype[Comment.Content]
- implicit val commentRepliesNrCodec: JsCodec[Comment.RepliesNr] =
- summonCodec[Int](JsonCodecMaker.make).refine[NonNegative].asNewtype[Comment.RepliesNr]
+ given JsCodec[Comment.Content] = newtypeCodec
+ given JsCodec[Comment.RepliesNr] = newtypeCodec
- // properties schemas
- implicit val commentContentSchema: JsSchema[Comment.Content] =
- summonSchema[String].asNewtype[Comment.Content]
- implicit val commentRepliesNrSchema: JsSchema[Comment.RepliesNr] =
- summonSchema[Int Refined NonNegative].asNewtype[Comment.RepliesNr]
-
- @Semi(JsCodec, JsSchema) sealed trait CommentError extends ADT
+ sealed trait CommentError derives DefaultJsCodec, JsSchema
object CommentError {
- @Semi(JsCodec, JsSchema) final case class BadCredentials(msg: String) extends CommentError
- @Semi(JsCodec, JsSchema) final case class NoPermission(msg: String) extends CommentError
- @Semi(JsCodec, JsSchema) final case class NotFound(msg: String) extends CommentError
- @Semi(JsCodec, JsSchema) final case class ValidationFailed(error: NonEmptyList[String]) extends CommentError
+ final case class BadCredentials(msg: String) extends CommentError derives DefaultJsCodec, JsSchema
+ final case class NoPermission(msg: String) extends CommentError derives DefaultJsCodec, JsSchema
+ final case class NotFound(msg: String) extends CommentError derives DefaultJsCodec, JsSchema
+ final case class ValidationFailed(error: NonEmptyList[String]) extends CommentError derives DefaultJsCodec, JsSchema
}
- @Semi(JsCodec, JsSchema) final case class APIComment(
+ final case class APIComment(
id: ID[Comment],
authorID: ID[User],
channelID: ID[Channel],
@@ -44,27 +30,30 @@ object CommentModels {
content: Comment.Content,
replyTo: Option[ID[Comment]],
repliesNr: Comment.RepliesNr
- )
+ ) derives DefaultJsCodec,
+ JsSchema
object APIComment {
def fromDomain(comment: Comment): APIComment =
comment.data.into[APIComment].withFieldConst(_.id, comment.id).transform
}
- @Semi(JsCodec, JsSchema) final case class CreateCommentRequest(
+ final case class CreateCommentRequest(
content: Comment.Content,
replyTo: Option[ID[Comment]]
- )
+ ) derives DefaultJsCodec,
+ JsSchema
- @Semi(JsCodec, JsSchema) final case class CreateCommentResponse(id: ID[Comment])
+ final case class CreateCommentResponse(id: ID[Comment]) derives DefaultJsCodec, JsSchema
- @Semi(JsCodec, JsSchema) final case class UpdateCommentRequest(
+ final case class UpdateCommentRequest(
newContent: Updatable[Comment.Content]
- )
+ ) derives DefaultJsCodec,
+ JsSchema
- @Semi(JsCodec, JsSchema) final case class UpdateCommentResponse(id: ID[Comment])
+ final case class UpdateCommentResponse(id: ID[Comment]) derives DefaultJsCodec, JsSchema
- @Semi(JsCodec, JsSchema) final case class DeleteCommentResponse(id: ID[Comment])
+ final case class DeleteCommentResponse(id: ID[Comment]) derives DefaultJsCodec, JsSchema
- @Semi(JsCodec, JsSchema) final case class RestoreCommentResponse(id: ID[Comment])
+ final case class RestoreCommentResponse(id: ID[Comment]) derives DefaultJsCodec, JsSchema
}
diff --git a/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/PostAPIs.scala b/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/PostAPIs.scala
index f83302bd..31a1d51f 100644
--- a/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/PostAPIs.scala
+++ b/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/PostAPIs.scala
@@ -2,10 +2,10 @@ package io.branchtalk.discussions.api
import java.net.URI
-import io.branchtalk.api._
-import io.branchtalk.api.AuthenticationSupport._
-import io.branchtalk.api.TapirSupport._
-import io.branchtalk.discussions.api.PostModels._
+import io.branchtalk.api.*
+import io.branchtalk.api.AuthenticationSupport.{ *, given }
+import io.branchtalk.api.TapirSupport.{ *, given }
+import io.branchtalk.discussions.api.PostModels.*
import io.branchtalk.discussions.model.{ Channel, Post }
import io.branchtalk.shared.model.{ ID, Updatable }
import sttp.model.StatusCode
@@ -23,7 +23,7 @@ object PostAPIs {
val newest: AuthedEndpoint[
Option[Authentication],
- (ID[Channel], Option[PaginationOffset], Option[PaginationLimit]),
+ (ID[Channel], Option[Pagination.Offset], Option[Pagination.Limit]),
PostError,
Pagination[APIPost],
Any
@@ -35,8 +35,8 @@ object PostAPIs {
.get
.securityIn(optAuthHeader)
.in(prefix / "newest")
- .in(query[Option[PaginationOffset]]("offset"))
- .in(query[Option[PaginationLimit]]("limit"))
+ .in(query[Option[Pagination.Offset]]("offset"))
+ .in(query[Option[Pagination.Limit]]("limit"))
.out(jsonBody[Pagination[APIPost]])
.errorOut(errorMapping)
.notRequiringPermissions
@@ -139,7 +139,7 @@ object PostAPIs {
.out(jsonBody[UpdatePostResponse])
.errorOut(errorMapping)
.requiringPermissions { case (channelID, _, _) =>
- RequiredPermissions.anyOf(Permission.IsOwner, Permission.ModerateChannel(ChannelID(channelID.uuid)))
+ RequiredPermissions.anyOf(Permission.IsOwner, Permission.ModerateChannel(ChannelID(channelID.unwrap)))
}
val delete: AuthedEndpoint[
@@ -159,7 +159,7 @@ object PostAPIs {
.out(jsonBody[DeletePostResponse])
.errorOut(errorMapping)
.requiringPermissions { case (channelID, _) =>
- RequiredPermissions.anyOf(Permission.IsOwner, Permission.ModerateChannel(ChannelID(channelID.uuid)))
+ RequiredPermissions.anyOf(Permission.IsOwner, Permission.ModerateChannel(ChannelID(channelID.unwrap)))
}
val restore: AuthedEndpoint[
@@ -179,7 +179,7 @@ object PostAPIs {
.out(jsonBody[RestorePostResponse])
.errorOut(errorMapping)
.requiringPermissions { case (channelID, _) =>
- RequiredPermissions.anyOf(Permission.IsOwner, Permission.ModerateChannel(ChannelID(channelID.uuid)))
+ RequiredPermissions.anyOf(Permission.IsOwner, Permission.ModerateChannel(ChannelID(channelID.unwrap)))
}
val upvote: AuthedEndpoint[
diff --git a/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/PostModels.scala b/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/PostModels.scala
index d50a5f24..fdc82670 100644
--- a/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/PostModels.scala
+++ b/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/PostModels.scala
@@ -3,17 +3,12 @@ package io.branchtalk.discussions.api
import java.net.URI
import cats.data.NonEmptyList
-import com.github.plokhotnyuk.jsoniter_scala.macros._
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.collection.NonEmpty
-import eu.timepit.refined.numeric.NonNegative
-import io.branchtalk.ADT
-import io.branchtalk.api.JsoniterSupport._
-import io.branchtalk.api.TapirSupport._
-import io.branchtalk.discussions.model._
-import io.branchtalk.shared.model.{ ID, Updatable, discriminatorNameMapper }
-import io.scalaland.catnip.Semi
-import io.scalaland.chimney.dsl._
+import com.github.plokhotnyuk.jsoniter_scala.macros.*
+import io.branchtalk.api.JsoniterSupport.{ *, given }
+import io.branchtalk.api.TapirSupport.{ *, given }
+import io.branchtalk.discussions.model.*
+import io.branchtalk.shared.model.*
+import io.scalaland.chimney.dsl.*
import sttp.tapir.Schema
import sttp.tapir.generic.Configuration
@@ -24,78 +19,67 @@ import scala.util.Try
object PostModels {
// properties codecs
- implicit val postUrlTitleCodec: JsCodec[Post.UrlTitle] =
- summonCodec[String](JsonCodecMaker.make).refine[NonEmpty].asNewtype[Post.UrlTitle]
- implicit val postTitleCodec: JsCodec[Post.Title] =
- summonCodec[String](JsonCodecMaker.make).refine[NonEmpty].asNewtype[Post.Title]
- implicit val postURLCodec: JsCodec[Post.URL] =
- summonCodec[String](JsonCodecMaker.make)
+ given JsCodec[Post.UrlTitle] = newtypeCodec
+ given JsCodec[Post.Title] = newtypeCodec
+ given JsCodec[Post.URL] =
+ DefaultJsCodec
+ .derived[String]
.mapDecode[URI](s => Try(URI.create(s)).fold(_ => Left(s"Invalid URI: $s"), Right(_)))(_.toString)
- .asNewtype[Post.URL]
- implicit val postTextCodec: JsCodec[Post.Text] =
- summonCodec[String](JsonCodecMaker.make).asNewtype[Post.Text]
- implicit val postContentCodec: JsCodec[Post.Content] =
- summonCodec[Post.Content](
- JsonCodecMaker.make(CodecMakerConfig.withAdtLeafClassNameMapper(discriminatorNameMapper(".")))
- )
- implicit val postRepliesNrCodec: JsCodec[Post.CommentsNr] =
- summonCodec[Int](JsonCodecMaker.make).refine[NonNegative].asNewtype[Post.CommentsNr]
+ .asNewtypeCodec[Post.URL]
+ given JsCodec[Post.Text] = newtypeCodec
+ given JsCodec[Post.Content] = {
+ inline given CodecMakerConfig = CodecMakerConfig.withAdtLeafClassNameMapper(adtDiscriminatorNameMapper)
+ DefaultJsCodec.derived[Post.Content]
+ }
+ given JsCodec[Post.CommentsNr] = newtypeCodec
// properties schemas
- implicit val postUrlTitleSchema: JsSchema[Post.UrlTitle] =
- summonSchema[String Refined NonEmpty].asNewtype[Post.UrlTitle]
- implicit val postTitleSchema: JsSchema[Post.Title] =
- summonSchema[String Refined NonEmpty].asNewtype[Post.Title]
- implicit val postURLSchema: JsSchema[Post.URL] =
- summonSchema[URI].asNewtype[Post.URL]
- implicit val postTextSchema: JsSchema[Post.Text] =
- summonSchema[String].asNewtype[Post.Text]
- implicit val postContentSchema: JsSchema[Post.Content] = {
+ given JsSchema[Post.Content] = {
// used in macros
- @unused implicit val customConfiguration: Configuration =
- Configuration.default.copy(toEncodedName = discriminatorNameMapper("."))
+ @unused given Configuration = Configuration.default.copy(toEncodedName = adtDiscriminatorNameMapper)
Schema.derived[Post.Content]
}
- implicit val postCommentsNrSchema: JsSchema[Post.CommentsNr] =
- summonSchema[Int Refined NonNegative].asNewtype[Post.CommentsNr]
- @Semi(JsCodec, JsSchema) sealed trait PostError extends ADT
+ sealed trait PostError derives DefaultJsCodec, JsSchema
object PostError {
- @Semi(JsCodec, JsSchema) final case class BadCredentials(msg: String) extends PostError
- @Semi(JsCodec, JsSchema) final case class NoPermission(msg: String) extends PostError
- @Semi(JsCodec, JsSchema) final case class NotFound(msg: String) extends PostError
- @Semi(JsCodec, JsSchema) final case class ValidationFailed(error: NonEmptyList[String]) extends PostError
+ final case class BadCredentials(msg: String) extends PostError derives DefaultJsCodec, JsSchema
+ final case class NoPermission(msg: String) extends PostError derives DefaultJsCodec, JsSchema
+ final case class NotFound(msg: String) extends PostError derives DefaultJsCodec, JsSchema
+ final case class ValidationFailed(error: NonEmptyList[String]) extends PostError derives DefaultJsCodec, JsSchema
}
- @Semi(JsCodec, JsSchema) final case class APIPost(
+ final case class APIPost(
id: ID[Post],
channelID: ID[Channel],
urlTitle: Post.UrlTitle,
title: Post.Title,
content: Post.Content,
commentsNr: Post.CommentsNr
- )
+ ) derives DefaultJsCodec,
+ JsSchema
object APIPost {
def fromDomain(post: Post): APIPost = post.data.into[APIPost].withFieldConst(_.id, post.id).transform
}
- @Semi(JsCodec, JsSchema) final case class CreatePostRequest(
+ final case class CreatePostRequest(
title: Post.Title,
content: Post.Content
- )
+ ) derives DefaultJsCodec,
+ JsSchema
- @Semi(JsCodec, JsSchema) final case class CreatePostResponse(id: ID[Post])
+ final case class CreatePostResponse(id: ID[Post]) derives DefaultJsCodec, JsSchema
- @Semi(JsCodec, JsSchema) final case class UpdatePostRequest(
+ final case class UpdatePostRequest(
newTitle: Updatable[Post.Title],
newContent: Updatable[Post.Content]
- )
+ ) derives DefaultJsCodec,
+ JsSchema
- @Semi(JsCodec, JsSchema) final case class UpdatePostResponse(id: ID[Post])
+ final case class UpdatePostResponse(id: ID[Post]) derives DefaultJsCodec, JsSchema
- @Semi(JsCodec, JsSchema) final case class DeletePostResponse(id: ID[Post])
+ final case class DeletePostResponse(id: ID[Post]) derives DefaultJsCodec, JsSchema
- @Semi(JsCodec, JsSchema) final case class RestorePostResponse(id: ID[Post])
+ final case class RestorePostResponse(id: ID[Post]) derives DefaultJsCodec, JsSchema
}
diff --git a/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/SubscriptionAPIs.scala b/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/SubscriptionAPIs.scala
index 467d88ae..6e6e7bce 100644
--- a/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/SubscriptionAPIs.scala
+++ b/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/SubscriptionAPIs.scala
@@ -1,10 +1,10 @@
package io.branchtalk.discussions.api
-import io.branchtalk.api._
-import io.branchtalk.api.AuthenticationSupport._
-import io.branchtalk.api.TapirSupport._
-import io.branchtalk.discussions.api.PostModels._
-import io.branchtalk.discussions.api.SubscriptionModels._
+import io.branchtalk.api.*
+import io.branchtalk.api.AuthenticationSupport.{ *, given }
+import io.branchtalk.api.TapirSupport.{ *, given }
+import io.branchtalk.discussions.api.PostModels.*
+import io.branchtalk.discussions.api.SubscriptionModels.*
import sttp.model.StatusCode
object SubscriptionAPIs {
@@ -22,7 +22,7 @@ object SubscriptionAPIs {
val newest: AuthedEndpoint[
Option[Authentication],
- (Option[PaginationOffset], Option[PaginationLimit]),
+ (Option[Pagination.Offset], Option[Pagination.Limit]),
PostError,
Pagination[APIPost],
Any
@@ -34,8 +34,8 @@ object SubscriptionAPIs {
.get
.securityIn(optAuthHeader)
.in(prefix / "newest")
- .in(query[Option[PaginationOffset]]("offset"))
- .in(query[Option[PaginationLimit]]("limit"))
+ .in(query[Option[Pagination.Offset]]("offset"))
+ .in(query[Option[Pagination.Limit]]("limit"))
.out(jsonBody[Pagination[APIPost]])
.errorOut(PostAPIs.errorMapping) // an exception in our API
.notRequiringPermissions
diff --git a/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/SubscriptionModels.scala b/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/SubscriptionModels.scala
index 16f9f5c8..40612021 100644
--- a/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/SubscriptionModels.scala
+++ b/modules/discussions-api/src/main/scala/io/branchtalk/discussions/api/SubscriptionModels.scala
@@ -1,32 +1,29 @@
package io.branchtalk.discussions.api
import cats.data.NonEmptyList
-import io.branchtalk.ADT
-import io.branchtalk.api.JsoniterSupport._
-import io.branchtalk.api.TapirSupport._
+import io.branchtalk.api.JsoniterSupport.{ *, given }
+import io.branchtalk.api.TapirSupport.{ *, given }
import io.branchtalk.discussions.model.Channel
import io.branchtalk.shared.model.ID
-import io.scalaland.catnip.Semi
-@SuppressWarnings(Array("org.wartremover.warts.All")) // for macros
object SubscriptionModels {
- @Semi(JsCodec, JsSchema) sealed trait SubscriptionError extends ADT
+ sealed trait SubscriptionError derives DefaultJsCodec, JsSchema
object SubscriptionError {
- @Semi(JsCodec, JsSchema) final case class BadCredentials(msg: String) extends SubscriptionError
- @Semi(JsCodec, JsSchema) final case class NoPermission(msg: String) extends SubscriptionError
- @Semi(JsCodec, JsSchema) final case class NotFound(msg: String) extends SubscriptionError
- @Semi(JsCodec, JsSchema) final case class ValidationFailed(error: NonEmptyList[String]) extends SubscriptionError
+ final case class BadCredentials(msg: String) extends SubscriptionError derives DefaultJsCodec, JsSchema
+ final case class NoPermission(msg: String) extends SubscriptionError derives DefaultJsCodec, JsSchema
+ final case class NotFound(msg: String) extends SubscriptionError derives DefaultJsCodec, JsSchema
+ final case class ValidationFailed(error: NonEmptyList[String]) extends SubscriptionError
+ derives DefaultJsCodec,
+ JsSchema
}
- @Semi(JsCodec, JsSchema) final case class APISubscriptions(channels: List[ID[Channel]])
+ final case class APISubscriptions(channels: List[ID[Channel]]) derives DefaultJsCodec, JsSchema
- @Semi(JsCodec, JsSchema) final case class SubscribeRequest(channels: List[ID[Channel]])
+ final case class SubscribeRequest(channels: List[ID[Channel]]) derives DefaultJsCodec, JsSchema
+ final case class SubscribeResponse(channels: List[ID[Channel]]) derives DefaultJsCodec, JsSchema
- @Semi(JsCodec, JsSchema) final case class SubscribeResponse(channels: List[ID[Channel]])
-
- @Semi(JsCodec, JsSchema) final case class UnsubscribeRequest(channels: List[ID[Channel]])
-
- @Semi(JsCodec, JsSchema) final case class UnsubscribeResponse(channels: List[ID[Channel]])
+ final case class UnsubscribeRequest(channels: List[ID[Channel]]) derives DefaultJsCodec, JsSchema
+ final case class UnsubscribeResponse(channels: List[ID[Channel]]) derives DefaultJsCodec, JsSchema
}
diff --git a/modules/discussions-impl/src/it/scala/io/branchtalk/discussions/DiscussionsFixtures.scala b/modules/discussions-impl/src/it/scala/io/branchtalk/discussions/DiscussionsFixtures.scala
deleted file mode 100644
index eb84d6f2..00000000
--- a/modules/discussions-impl/src/it/scala/io/branchtalk/discussions/DiscussionsFixtures.scala
+++ /dev/null
@@ -1,39 +0,0 @@
-package io.branchtalk.discussions
-
-import cats.effect.IO
-import io.branchtalk.discussions.model._
-import io.branchtalk.shared.model.{ ID, UUIDGenerator }
-import io.branchtalk.shared.Fixtures._
-
-trait DiscussionsFixtures {
-
- def editorIDCreate(implicit uuidGenerator: UUIDGenerator): IO[ID[User]] = ID.create[IO, User]
-
- def subscriberIDCreate(implicit uuidGenerator: UUIDGenerator): IO[ID[User]] = ID.create[IO, User]
-
- def voterIDCreate(implicit uuidGenerator: UUIDGenerator): IO[ID[User]] = ID.create[IO, User]
-
- def channelCreate(implicit uuidGenerator: UUIDGenerator): IO[Channel.Create] =
- (
- ID.create[IO, User],
- noWhitespaces.flatMap(Channel.UrlName.parse[IO]),
- nameLike.flatMap(Channel.Name.parse[IO]),
- textProducer.map(_.loremIpsum).flatMap(Channel.Description.parse[IO]).map(Option.apply)
- ).mapN(Channel.Create.apply)
-
- def postCreate(channelID: ID[Channel])(implicit uuidGenerator: UUIDGenerator): IO[Post.Create] =
- (
- ID.create[IO, User],
- channelID.pure[IO],
- nameLike.flatMap(Post.Title.parse[IO]),
- textProducer.map(_.loremIpsum).map(Post.Text(_)).map(Post.Content.Text(_))
- ).mapN(Post.Create.apply)
-
- def commentCreate(postID: ID[Post])(implicit uuidGenerator: UUIDGenerator): IO[Comment.Create] =
- (
- ID.create[IO, User],
- postID.pure[IO],
- textProducer.map(_.loremIpsum).map(Comment.Content(_)),
- none[ID[Comment]].pure[IO]
- ).mapN(Comment.Create.apply)
-}
diff --git a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/DiscussionsModule.scala b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/DiscussionsModule.scala
index a8fb06dd..0f4d7f3c 100644
--- a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/DiscussionsModule.scala
+++ b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/DiscussionsModule.scala
@@ -4,11 +4,11 @@ import cats.data.NonEmptyList
import cats.effect.{ Async, Resource }
import cats.effect.std.Dispatcher
import io.branchtalk.discussions.events.{ DiscussionEvent, DiscussionsCommandEvent }
-import io.branchtalk.discussions.reads._
-import io.branchtalk.discussions.writes._
-import io.branchtalk.shared.model._
-import io.branchtalk.shared.infrastructure._
-import com.softwaremill.macwire.wire
+import io.branchtalk.discussions.reads.*
+import io.branchtalk.discussions.writes.*
+import io.branchtalk.logging.*
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.infrastructure.*
import io.branchtalk.logging.MDC
import io.prometheus.client.CollectorRegistry
@@ -30,7 +30,6 @@ final case class DiscussionsWrites[F[_]](
runProjecions: StreamRunner[F]
)
-@nowarn("cat=unused") // macwire
object DiscussionsModule {
private val module = DomainModule[DiscussionEvent, DiscussionsCommandEvent]
@@ -39,69 +38,74 @@ object DiscussionsModule {
val postgresProjectionName = "postgres-projection"
def reads[F[_]: Async](
- domainConfig: DomainConfig,
+ domainConfig: DomainModule.Config,
registry: CollectorRegistry
): Resource[F, DiscussionsReads[F]] =
- Logger.getLogger[F].pipe { logger =>
- Resource.make(logger.info("Initialize Discussions reads"))(_ => logger.info("Shut down Discussions reads"))
- } >>
- module.setupReads[F](domainConfig, registry).map { case ReadsInfrastructure(transactor, consumer) =>
- val channelReads: ChannelReads[F] = wire[ChannelReadsImpl[F]]
- val postReads: PostReads[F] = wire[PostReadsImpl[F]]
- val commentReads: CommentReads[F] = wire[CommentReadsImpl[F]]
- val subscriptionReads: SubscriptionReads[F] = wire[SubscriptionReadsImpl[F]]
+ for {
+ logger <- Resource.eval(Logger.create[F])
+ _ <- Resource.make(logger.info("Initialize Discussions reads"))(_ => logger.info("Shut down Discussions reads"))
+ case Reads.Infrastructure(transactor, consumer) <- module.setupReads[F](domainConfig, logger, registry)
+ } yield {
+ // macwire got removed due to:
+ // https://github.com/softwaremill/macwire/blob/abf95284e24138a984a06ef4f08d0788af371825/macros/src/main/scala-3/com/softwaremill/macwire/internals/ConstructorCrimper.scala#L91
+ val channelReads: ChannelReads[F] = ChannelReadsImpl[F](transactor)
+ val postReads: PostReads[F] = PostReadsImpl[F](transactor)
+ val commentReads: CommentReads[F] = CommentReadsImpl[F](transactor)
+ val subscriptionReads: SubscriptionReads[F] = SubscriptionReadsImpl[F](transactor)
- wire[DiscussionsReads[F]]
- }
+ DiscussionsReads(channelReads, postReads, commentReads, subscriptionReads, consumer)
+ }
def writes[F[_]: Async: Dispatcher: MDC](
- domainConfig: DomainConfig,
- registry: CollectorRegistry
- )(implicit uuidGenerator: UUIDGenerator): Resource[F, DiscussionsWrites[F]] =
- Logger.getLogger[F].pipe { logger =>
- Resource.make(logger.info("Initialize Discussions writes"))(_ => logger.info("Shut down Discussions writes")) >>
- module.setupWrites[F](domainConfig, registry).map {
- case WritesInfrastructure(transactor,
- internalProducer,
- internalConsumerStream,
- producer,
- consumerStream,
- cache
- ) =>
- val channelWrites: ChannelWrites[F] = wire[ChannelWritesImpl[F]]
- val postWrites: PostWrites[F] = wire[PostWritesImpl[F]]
- val commentWrites: CommentWrites[F] = wire[CommentWritesImpl[F]]
- val subscriptionWrites: SubscriptionWrites[F] = wire[SubscriptionWritesImpl[F]]
+ domainConfig: DomainModule.Config,
+ registry: CollectorRegistry
+ )(using UUID.Generator): Resource[F, DiscussionsWrites[F]] =
+ for {
+ logger <- Resource.eval(Logger.create[F])
+ _ <- Resource.make(logger.info("Initialize Discussions writes"))(_ => logger.info("Shut down Discussions writes"))
+ case Writes.Infrastructure(transactor,
+ internalProducer,
+ internalConsumerStream,
+ producer,
+ consumerStream,
+ cache
+ ) <- module.setupWrites[F](domainConfig, logger, registry)
+ } yield {
+ // macwire got removed due to:
+ // https://github.com/softwaremill/macwire/blob/abf95284e24138a984a06ef4f08d0788af371825/macros/src/main/scala-3/com/softwaremill/macwire/internals/ConstructorCrimper.scala#L91
+ val channelWrites: ChannelWrites[F] = ChannelWritesImpl[F](internalProducer, transactor)
+ val postWrites: PostWrites[F] = PostWritesImpl[F](internalProducer, transactor)
+ val commentWrites: CommentWrites[F] = CommentWritesImpl[F](internalProducer, transactor)
+ val subscriptionWrites: SubscriptionWrites[F] = SubscriptionWritesImpl[F](internalProducer, transactor)
- val commandHandler: Projector[F, DiscussionsCommandEvent, (UUID, DiscussionEvent)] = NonEmptyList
- .of(
- wire[ChannelCommandHandler[F]],
- wire[PostCommandHandler[F]],
- wire[CommentCommandHandler[F]],
- wire[SubscriptionCommandHandler[F]]
- )
- .reduce
- val postgresProjector: Projector[F, DiscussionEvent, (UUID, DiscussionEvent)] = NonEmptyList
- .of(
- wire[ChannelPostgresProjector[F]],
- wire[CommentPostgresProjector[F]],
- wire[PostPostgresProjector[F]],
- wire[SubscriptionPostgresProjector[F]]
- )
- .reduce
- val runProjector: StreamRunner[F] = {
- val runCommandProjector: StreamRunner[F] =
- internalConsumerStream.runCachedThrough(logger, cache)(
- ConsumerStream.noID.andThen(commandHandler).andThen(producer).andThen(ConsumerStream.produced)
- )
- val runPostgresProjector: StreamRunner[F] =
- consumerStream(domainConfig.consumers(postgresProjectionName)).runCachedThrough(logger, cache)(
- ConsumerStream.noID.andThen(postgresProjector).andThen(ConsumerStream.noID)
- )
- runCommandProjector |+| runPostgresProjector
- }
+ val commandHandler: Projector[F, DiscussionsCommandEvent, (UUID, DiscussionEvent)] = NonEmptyList
+ .of(
+ ChannelCommandHandler[F],
+ PostCommandHandler[F],
+ CommentCommandHandler[F],
+ SubscriptionCommandHandler[F]
+ )
+ .reduce
+ val postgresProjector: Projector[F, DiscussionEvent, (UUID, DiscussionEvent)] = NonEmptyList
+ .of(
+ ChannelPostgresProjector[F](transactor),
+ CommentPostgresProjector[F](transactor),
+ PostPostgresProjector[F](transactor),
+ SubscriptionPostgresProjector[F](transactor)
+ )
+ .reduce
+ val runProjector: StreamRunner[F] = {
+ val runCommandProjector: StreamRunner[F] =
+ internalConsumerStream.runCachedThrough(logger, cache)(
+ ConsumerStream.noID.andThen(commandHandler).andThen(producer).andThen(ConsumerStream.produced)
+ )
+ val runPostgresProjector: StreamRunner[F] =
+ consumerStream(domainConfig.consumers(postgresProjectionName)).runCachedThrough(logger, cache)(
+ ConsumerStream.noID.andThen(postgresProjector).andThen(ConsumerStream.noID)
+ )
+ runCommandProjector |+| runPostgresProjector
+ }
- wire[DiscussionsWrites[F]]
- }
+ DiscussionsWrites(commentWrites, postWrites, channelWrites, subscriptionWrites, runProjector)
}
}
diff --git a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/events/ChannelCommandEvent.scala b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/events/ChannelCommandEvent.scala
index 8351c1af..85e413f6 100644
--- a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/events/ChannelCommandEvent.scala
+++ b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/events/ChannelCommandEvent.scala
@@ -1,17 +1,15 @@
package io.branchtalk.discussions.events
-import com.sksamuel.avro4s._
-import io.scalaland.catnip.Semi
-import io.branchtalk.ADT
+import com.sksamuel.avro4s.*
import io.branchtalk.discussions.model.{ Channel, User }
import io.branchtalk.logging.CorrelationID
-import io.branchtalk.shared.model._
-import io.branchtalk.shared.model.AvroSupport._
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.model.AvroSupport.{ *, given }
-@Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) sealed trait ChannelCommandEvent extends ADT
+sealed trait ChannelCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
object ChannelCommandEvent {
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Create(
+ final case class Create(
id: ID[Channel],
authorID: ID[User],
urlName: Channel.UrlName,
@@ -19,9 +17,9 @@ object ChannelCommandEvent {
description: Option[Channel.Description],
createdAt: CreationTime,
correlationID: CorrelationID
- ) extends ChannelCommandEvent
+ ) extends ChannelCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Update(
+ final case class Update(
id: ID[Channel],
editorID: ID[User],
newUrlName: Updatable[Channel.UrlName],
@@ -29,17 +27,17 @@ object ChannelCommandEvent {
newDescription: OptionUpdatable[Channel.Description],
modifiedAt: ModificationTime,
correlationID: CorrelationID
- ) extends ChannelCommandEvent
+ ) extends ChannelCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Delete(
+ final case class Delete(
id: ID[Channel],
editorID: ID[User],
correlationID: CorrelationID
- ) extends ChannelCommandEvent
+ ) extends ChannelCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Restore(
+ final case class Restore(
id: ID[Channel],
editorID: ID[User],
correlationID: CorrelationID
- ) extends ChannelCommandEvent
+ ) extends ChannelCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
}
diff --git a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/events/CommentCommandEvent.scala b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/events/CommentCommandEvent.scala
index 23a27b57..b5577d9f 100644
--- a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/events/CommentCommandEvent.scala
+++ b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/events/CommentCommandEvent.scala
@@ -1,17 +1,15 @@
package io.branchtalk.discussions.events
-import com.sksamuel.avro4s._
-import io.scalaland.catnip.Semi
-import io.branchtalk.ADT
+import com.sksamuel.avro4s.*
import io.branchtalk.discussions.model.{ Channel, Comment, Post, User }
import io.branchtalk.logging.CorrelationID
-import io.branchtalk.shared.model._
-import io.branchtalk.shared.model.AvroSupport._
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.model.AvroSupport.{ *, given }
-@Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) sealed trait CommentCommandEvent extends ADT
+sealed trait CommentCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
object CommentCommandEvent {
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Create(
+ final case class Create(
id: ID[Comment],
authorID: ID[User],
channelID: ID[Channel],
@@ -20,43 +18,43 @@ object CommentCommandEvent {
replyTo: Option[ID[Comment]],
createdAt: CreationTime,
correlationID: CorrelationID
- ) extends CommentCommandEvent
+ ) extends CommentCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Update(
+ final case class Update(
id: ID[Comment],
editorID: ID[User],
newContent: Updatable[Comment.Content],
modifiedAt: ModificationTime,
correlationID: CorrelationID
- ) extends CommentCommandEvent
+ ) extends CommentCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Delete(
+ final case class Delete(
id: ID[Comment],
editorID: ID[User],
correlationID: CorrelationID
- ) extends CommentCommandEvent
+ ) extends CommentCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Restore(
+ final case class Restore(
id: ID[Comment],
editorID: ID[User],
correlationID: CorrelationID
- ) extends CommentCommandEvent
+ ) extends CommentCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Upvote(
+ final case class Upvote(
id: ID[Comment],
voterID: ID[User],
correlationID: CorrelationID
- ) extends CommentCommandEvent
+ ) extends CommentCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Downvote(
+ final case class Downvote(
id: ID[Comment],
voterID: ID[User],
correlationID: CorrelationID
- ) extends CommentCommandEvent
+ ) extends CommentCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class RevokeVote(
+ final case class RevokeVote(
id: ID[Comment],
voterID: ID[User],
correlationID: CorrelationID
- ) extends CommentCommandEvent
+ ) extends CommentCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
}
diff --git a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/events/DiscussionsCommandEvent.scala b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/events/DiscussionsCommandEvent.scala
index 5c54fd43..2edb6da2 100644
--- a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/events/DiscussionsCommandEvent.scala
+++ b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/events/DiscussionsCommandEvent.scala
@@ -1,15 +1,33 @@
package io.branchtalk.discussions.events
-import com.sksamuel.avro4s._
-import io.scalaland.catnip.Semi
-import io.branchtalk.ADT
-import io.branchtalk.shared.model._
-import io.branchtalk.shared.model.AvroSupport._
+import com.sksamuel.avro4s.*
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.model.AvroSupport.{ *, given }
-@Semi(Decoder, Encoder, ShowPretty, SchemaFor) sealed trait DiscussionsCommandEvent extends ADT
+sealed trait DiscussionsCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
object DiscussionsCommandEvent {
final case class ForChannel(channel: ChannelCommandEvent) extends DiscussionsCommandEvent
+ derives Decoder,
+ Encoder,
+ FastEq,
+ ShowPretty,
+ SchemaFor
final case class ForComment(comment: CommentCommandEvent) extends DiscussionsCommandEvent
+ derives Decoder,
+ Encoder,
+ FastEq,
+ ShowPretty,
+ SchemaFor
final case class ForPost(post: PostCommandEvent) extends DiscussionsCommandEvent
+ derives Decoder,
+ Encoder,
+ FastEq,
+ ShowPretty,
+ SchemaFor
final case class ForSubscription(subscription: SubscriptionCommandEvent) extends DiscussionsCommandEvent
+ derives Decoder,
+ Encoder,
+ FastEq,
+ ShowPretty,
+ SchemaFor
}
diff --git a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/events/PostCommandEvent.scala b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/events/PostCommandEvent.scala
index 93da777f..222100b0 100644
--- a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/events/PostCommandEvent.scala
+++ b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/events/PostCommandEvent.scala
@@ -1,17 +1,15 @@
package io.branchtalk.discussions.events
-import com.sksamuel.avro4s._
-import io.scalaland.catnip.Semi
-import io.branchtalk.ADT
+import com.sksamuel.avro4s.*
import io.branchtalk.discussions.model.{ Channel, Post, User }
import io.branchtalk.logging.CorrelationID
-import io.branchtalk.shared.model._
-import io.branchtalk.shared.model.AvroSupport._
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.model.AvroSupport.{ *, given }
-@Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) sealed trait PostCommandEvent extends ADT
+sealed trait PostCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
object PostCommandEvent {
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Create(
+ final case class Create(
id: ID[Post],
authorID: ID[User],
channelID: ID[Channel],
@@ -20,9 +18,9 @@ object PostCommandEvent {
content: Post.Content,
createdAt: CreationTime,
correlationID: CorrelationID
- ) extends PostCommandEvent
+ ) extends PostCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Update(
+ final case class Update(
id: ID[Post],
editorID: ID[User],
newUrlTitle: Updatable[Post.UrlTitle],
@@ -30,35 +28,35 @@ object PostCommandEvent {
newContent: Updatable[Post.Content],
modifiedAt: ModificationTime,
correlationID: CorrelationID
- ) extends PostCommandEvent
+ ) extends PostCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Delete(
+ final case class Delete(
id: ID[Post],
editorID: ID[User],
correlationID: CorrelationID
- ) extends PostCommandEvent
+ ) extends PostCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Restore(
+ final case class Restore(
id: ID[Post],
editorID: ID[User],
correlationID: CorrelationID
- ) extends PostCommandEvent
+ ) extends PostCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Upvote(
+ final case class Upvote(
id: ID[Post],
voterID: ID[User],
correlationID: CorrelationID
- ) extends PostCommandEvent
+ ) extends PostCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Downvote(
+ final case class Downvote(
id: ID[Post],
voterID: ID[User],
correlationID: CorrelationID
- ) extends PostCommandEvent
+ ) extends PostCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class RevokeVote(
+ final case class RevokeVote(
id: ID[Post],
voterID: ID[User],
correlationID: CorrelationID
- ) extends PostCommandEvent
+ ) extends PostCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
}
diff --git a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/events/SubscriptionCommandEvent.scala b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/events/SubscriptionCommandEvent.scala
index d931fb23..eb9cbe33 100644
--- a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/events/SubscriptionCommandEvent.scala
+++ b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/events/SubscriptionCommandEvent.scala
@@ -1,27 +1,25 @@
package io.branchtalk.discussions.events
-import com.sksamuel.avro4s._
-import io.branchtalk.ADT
-import io.branchtalk.discussions.model._
+import com.sksamuel.avro4s.*
+import io.branchtalk.discussions.model.*
import io.branchtalk.logging.CorrelationID
-import io.branchtalk.shared.model._
-import io.branchtalk.shared.model.AvroSupport._
-import io.scalaland.catnip.Semi
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.model.AvroSupport.{ *, given }
-@Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) sealed trait SubscriptionCommandEvent extends ADT
+sealed trait SubscriptionCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
object SubscriptionCommandEvent {
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Subscribe(
+ final case class Subscribe(
subscriberID: ID[User],
subscriptions: Set[ID[Channel]],
modifiedAt: ModificationTime,
correlationID: CorrelationID
- ) extends SubscriptionCommandEvent
+ ) extends SubscriptionCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Unsubscribe(
+ final case class Unsubscribe(
subscriberID: ID[User],
subscriptions: Set[ID[Channel]],
modifiedAt: ModificationTime,
correlationID: CorrelationID
- ) extends SubscriptionCommandEvent
+ ) extends SubscriptionCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
}
diff --git a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/infrastructure/DoobieExtensions.scala b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/infrastructure/DoobieExtensions.scala
index a064fe66..50d8192b 100644
--- a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/infrastructure/DoobieExtensions.scala
+++ b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/infrastructure/DoobieExtensions.scala
@@ -1,14 +1,24 @@
package io.branchtalk.discussions.infrastructure
+import cats.Show
import io.branchtalk.discussions.model.{ Post, Vote }
-import io.branchtalk.shared.infrastructure.DoobieSupport._
+import io.branchtalk.shared.infrastructure.DoobieSupport.{ *, given }
import io.branchtalk.shared.model.branchtalkLocale
object DoobieExtensions {
- implicit val postContentTypeMeta: Meta[Post.Content.Type] =
- pgEnumString("post_content_type", Post.Content.Type.withNameInsensitive, _.entryName.toLowerCase(branchtalkLocale))
+ private given [A: Show]: Show[Array[A]] = _.map(_.show).mkString(", ")
- implicit val voteTypeMeta: Meta[Vote.Type] =
+ @SuppressWarnings(Array("org.wartremover.warts.Throw"))
+ given postContentTypeMeta: Meta[Post.Content.Type] = pgEnumString(
+ "post_content_type",
+ name =>
+ Post.Content.Type.values
+ .find(_.entryName.equalsIgnoreCase(name))
+ .getOrElse(throw new NoSuchElementException(show"$name is not a member of Enum (${Post.Content.Type.values})")),
+ _.entryName.toLowerCase(branchtalkLocale)
+ )
+
+ given voteTypeMeta: Meta[Vote.Type] =
pgEnumString("vote_type", Vote.Type.withNameInsensitive, _.entryName.toLowerCase(branchtalkLocale))
}
diff --git a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/model/PostDao.scala b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/model/PostDao.scala
index b058c9de..bc91dc80 100644
--- a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/model/PostDao.scala
+++ b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/model/PostDao.scala
@@ -1,7 +1,7 @@
package io.branchtalk.discussions.model
import io.branchtalk.shared.model.{ CreationTime, ID, ModificationTime }
-import io.scalaland.chimney.dsl._
+import io.scalaland.chimney.dsl.*
final case class PostDao(
id: ID[Post],
diff --git a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/reads/ChannelReadsImpl.scala b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/reads/ChannelReadsImpl.scala
index 8d0366a3..497b8009 100644
--- a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/reads/ChannelReadsImpl.scala
+++ b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/reads/ChannelReadsImpl.scala
@@ -1,16 +1,12 @@
package io.branchtalk.discussions.reads
import cats.effect.Sync
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.numeric.{ NonNegative, Positive }
import io.branchtalk.discussions.model.Channel
-import io.branchtalk.shared.infrastructure.DoobieSupport._
-import io.branchtalk.shared.model._
+import io.branchtalk.shared.infrastructure.DoobieSupport.{ *, given }
+import io.branchtalk.shared.model.*
final class ChannelReadsImpl[F[_]: Sync](transactor: Transactor[F]) extends ChannelReads[F] {
- implicit private val logHandler: LogHandler = doobieLogger(getClass)
-
private val commonSelect: Fragment =
fr"""SELECT id,
| url_name,
@@ -25,34 +21,41 @@ final class ChannelReadsImpl[F[_]: Sync](transactor: Transactor[F]) extends Chan
case Channel.Sorting.Alphabetically => fr"ORDER BY name ASC"
}
- private def idExists(id: ID[Channel]): Fragment = fr"id = ${id} AND deleted = FALSE"
+ private def idExists(id: ID[Channel]): Fragment = fr"id = $id AND deleted = FALSE"
- private def idDeleted(id: ID[Channel]): Fragment = fr"id = ${id} AND deleted = TRUE"
+ private def idDeleted(id: ID[Channel]): Fragment = fr"id = $id AND deleted = TRUE"
override def paginate(
sortBy: Channel.Sorting,
- offset: Long Refined NonNegative,
- limit: Int Refined Positive
+ offset: Paginated.Offset,
+ limit: Paginated.Limit
): F[Paginated[Channel]] =
(commonSelect ++ Fragments.whereAnd(fr"deleted = FALSE") ++ orderBy(sortBy))
- .paginate[Channel](offset, limit)
+ .paginate[Channel](offset,
+ limit,
+ show"Paginate Discussions' Channel from $offset taking $limit sorted by $sortBy"
+ )
.transact(transactor)
override def exists(id: ID[Channel]): F[Boolean] =
- (fr"SELECT 1 FROM channels WHERE" ++ idExists(id)).exists.transact(transactor)
+ (fr"SELECT 1 FROM channels WHERE" ++ idExists(id))
+ .exists(show"Discussions' Channel ID=$id exists")
+ .transact(transactor)
override def deleted(id: ID[Channel]): F[Boolean] =
- (fr"SELECT 1 FROM channels WHERE" ++ idDeleted(id)).exists.transact(transactor)
+ (fr"SELECT 1 FROM channels WHERE" ++ idDeleted(id))
+ .exists(show"Discussions' Channel ID=$id deleted")
+ .transact(transactor)
override def getById(id: ID[Channel], isDeleted: Boolean = false): F[Option[Channel]] =
(commonSelect ++ fr"WHERE" ++ (if (isDeleted) idDeleted(id) else idExists(id)))
- .query[Channel]
+ .queryWithLabel[Channel](show"Get Discussions' Channel by ID=$id")
.option
.transact(transactor)
override def requireById(id: ID[Channel], isDeleted: Boolean = false): F[Channel] =
(commonSelect ++ fr"WHERE" ++ (if (isDeleted) idDeleted(id) else idExists(id)))
- .query[Channel]
+ .queryWithLabel[Channel](show"Require Discussions' Channel by ID=$id")
.failNotFound("User", id)
.transact(transactor)
}
diff --git a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/reads/CommentReadsImpl.scala b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/reads/CommentReadsImpl.scala
index 72338c3a..0c75091f 100644
--- a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/reads/CommentReadsImpl.scala
+++ b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/reads/CommentReadsImpl.scala
@@ -1,16 +1,12 @@
package io.branchtalk.discussions.reads
import cats.effect.Sync
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.numeric.{ NonNegative, Positive }
import io.branchtalk.discussions.model.{ Comment, Post }
-import io.branchtalk.shared.infrastructure.DoobieSupport._
+import io.branchtalk.shared.infrastructure.DoobieSupport.{ *, given }
import io.branchtalk.shared.model.{ ID, Paginated }
final class CommentReadsImpl[F[_]: Sync](transactor: Transactor[F]) extends CommentReads[F] {
- implicit private val logHandler: LogHandler = doobieLogger(getClass)
-
private val commonSelect: Fragment =
fr"""SELECT id,
| author_id,
@@ -34,37 +30,46 @@ final class CommentReadsImpl[F[_]: Sync](transactor: Transactor[F]) extends Comm
case Comment.Sorting.Controversial => fr"ORDER by controversial_score DESC"
}
- private def idExists(id: ID[Comment]): Fragment = fr"id = ${id} AND deleted = FALSE"
+ private def idExists(id: ID[Comment]): Fragment = fr"id = $id AND deleted = FALSE"
- private def idDeleted(id: ID[Comment]): Fragment = fr"id = ${id} AND deleted = TRUE"
+ private def idDeleted(id: ID[Comment]): Fragment = fr"id = $id AND deleted = TRUE"
override def paginate(
post: ID[Post],
repliesTo: Option[ID[Comment]],
sortBy: Comment.Sorting,
- offset: Long Refined NonNegative,
- limit: Int Refined Positive
+ offset: Paginated.Offset,
+ limit: Paginated.Limit
): F[Paginated[Comment]] =
(commonSelect ++ Fragments.whereAndOpt(fr"post_id = $post".some,
repliesTo.map(parent => fr"reply_to = $parent"),
fr"deleted = FALSE".some
- ) ++ orderBy(sortBy)).paginate[Comment](offset, limit).transact(transactor)
+ ) ++ orderBy(sortBy))
+ .paginate[Comment](offset,
+ limit,
+ show"Paginate Discussions' Comment from $offset taking $limit sorted by $sortBy"
+ )
+ .transact(transactor)
override def exists(id: ID[Comment]): F[Boolean] =
- (fr"SELECT 1 FROM comments WHERE" ++ idExists(id)).exists.transact(transactor)
+ (fr"SELECT 1 FROM comments WHERE" ++ idExists(id))
+ .exists(show"Discussions' Comment ID=$id exists")
+ .transact(transactor)
override def deleted(id: ID[Comment]): F[Boolean] =
- (fr"SELECT 1 FROM comments WHERE" ++ idDeleted(id)).exists.transact(transactor)
+ (fr"SELECT 1 FROM comments WHERE" ++ idDeleted(id))
+ .exists(show"Discussions' Comment ID=$id deleted")
+ .transact(transactor)
override def getById(id: ID[Comment], isDeleted: Boolean = false): F[Option[Comment]] =
(commonSelect ++ fr"WHERE" ++ (if (isDeleted) idDeleted(id) else idExists(id)))
- .query[Comment]
+ .queryWithLabel[Comment](show"Get Discussions' Comment by ID=$id")
.option
.transact(transactor)
override def requireById(id: ID[Comment], isDeleted: Boolean = false): F[Comment] =
(commonSelect ++ fr"WHERE" ++ (if (isDeleted) idDeleted(id) else idExists(id)))
- .query[Comment]
+ .queryWithLabel[Comment](show"Require Discussions' Comment by ID=$id")
.failNotFound("Comment", id)
.transact(transactor)
}
diff --git a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/reads/PostReadsImpl.scala b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/reads/PostReadsImpl.scala
index 85b312ce..563ec9af 100644
--- a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/reads/PostReadsImpl.scala
+++ b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/reads/PostReadsImpl.scala
@@ -2,17 +2,13 @@ package io.branchtalk.discussions.reads
import cats.data.NonEmptySet
import cats.effect.Sync
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.numeric.{ NonNegative, Positive }
-import io.branchtalk.discussions.infrastructure.DoobieExtensions._
+import io.branchtalk.discussions.infrastructure.DoobieExtensions.{ *, given }
import io.branchtalk.discussions.model.{ Channel, Post, PostDao }
-import io.branchtalk.shared.infrastructure.DoobieSupport._
+import io.branchtalk.shared.infrastructure.DoobieSupport.{ *, given }
import io.branchtalk.shared.model.{ ID, Paginated }
final class PostReadsImpl[F[_]: Sync](transactor: Transactor[F]) extends PostReads[F] {
- implicit private val logHandler: LogHandler = doobieLogger(getClass)
-
private val commonSelect: Fragment =
fr"""SELECT id,
| author_id,
@@ -36,37 +32,39 @@ final class PostReadsImpl[F[_]: Sync](transactor: Transactor[F]) extends PostRea
case Post.Sorting.Controversial => fr"ORDER by controversial_score DESC"
}
- private def idExists(id: ID[Post]): Fragment = fr"id = ${id} AND deleted = FALSE"
+ private def idExists(id: ID[Post]): Fragment = fr"id = $id AND deleted = FALSE"
- private def idDeleted(id: ID[Post]): Fragment = fr"id = ${id} AND deleted = TRUE"
+ private def idDeleted(id: ID[Post]): Fragment = fr"id = $id AND deleted = TRUE"
override def paginate(
channels: NonEmptySet[ID[Channel]],
sortBy: Post.Sorting,
- offset: Long Refined NonNegative,
- limit: Int Refined Positive
+ offset: Paginated.Offset,
+ limit: Paginated.Limit
): F[Paginated[Post]] =
- (commonSelect ++ Fragments.whereAnd(Fragments.in(fr"channel_id", channels), fr"deleted = FALSE") ++ orderBy(sortBy))
- .paginate[PostDao](offset, limit)
+ (commonSelect ++ Fragments.whereAnd(Fragments.in(fr"channel_id", channels.toNonEmptyList),
+ fr"deleted = FALSE"
+ ) ++ orderBy(sortBy))
+ .paginate[PostDao](offset, limit, show"Paginate Discussions' Post from $offset taking $limit sorted by $sortBy")
.map(_.map(_.toDomain))
.transact(transactor)
override def exists(id: ID[Post]): F[Boolean] =
- (fr"SELECT 1 FROM posts WHERE" ++ idExists(id)).exists.transact(transactor)
+ (fr"SELECT 1 FROM posts WHERE" ++ idExists(id)).exists(show"Discussions' Post $id exists").transact(transactor)
override def deleted(id: ID[Post]): F[Boolean] =
- (fr"SELECT 1 FROM posts WHERE" ++ idDeleted(id)).exists.transact(transactor)
+ (fr"SELECT 1 FROM posts WHERE" ++ idDeleted(id)).exists(show"Discussions' Post ID=$id deleted").transact(transactor)
override def getById(id: ID[Post], isDeleted: Boolean = false): F[Option[Post]] =
(commonSelect ++ fr"WHERE" ++ (if (isDeleted) idDeleted(id) else idExists(id)))
- .query[PostDao]
+ .queryWithLabel[PostDao](show"Get Discussions' Post by ID=$id")
.map(_.toDomain)
.option
.transact(transactor)
override def requireById(id: ID[Post], isDeleted: Boolean = false): F[Post] =
(commonSelect ++ fr"WHERE" ++ (if (isDeleted) idDeleted(id) else idExists(id)))
- .query[PostDao]
+ .queryWithLabel[PostDao](show"Require Discussions' Post by ID=$id")
.map(_.toDomain)
.failNotFound("Post", id)
.transact(transactor)
diff --git a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/reads/SubscriptionReadsImpl.scala b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/reads/SubscriptionReadsImpl.scala
index c00ab9a0..e875851a 100644
--- a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/reads/SubscriptionReadsImpl.scala
+++ b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/reads/SubscriptionReadsImpl.scala
@@ -2,21 +2,19 @@ package io.branchtalk.discussions.reads
import cats.effect.Sync
import io.branchtalk.discussions.model.{ Subscription, User }
-import io.branchtalk.shared.infrastructure.DoobieSupport._
-import io.branchtalk.shared.model._
+import io.branchtalk.shared.infrastructure.DoobieSupport.{ *, given }
+import io.branchtalk.shared.model.*
final class SubscriptionReadsImpl[F[_]: Sync](transactor: Transactor[F]) extends SubscriptionReads[F] {
- implicit private val logHandler: LogHandler = doobieLogger(getClass)
-
private val commonSelect: Fragment =
fr"""SELECT subscriber_id,
| subscriptions_ids
|FROM subscriptions""".stripMargin
override def requireForUser(userID: ID[User]): F[Subscription] =
- (commonSelect ++ fr"WHERE subscriber_id = ${userID}")
- .query[Subscription]
+ (commonSelect ++ fr"WHERE subscriber_id = $userID")
+ .queryWithLabel[Subscription](show"Require Discussions' Subscription for User=$userID")
.option
.map(_.getOrElse(Subscription(userID, Set.empty)))
.transact(transactor)
diff --git a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/ChannelCommandHandler.scala b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/ChannelCommandHandler.scala
index c59dfb8d..4469da77 100644
--- a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/ChannelCommandHandler.scala
+++ b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/ChannelCommandHandler.scala
@@ -6,7 +6,7 @@ import fs2.Stream
import io.branchtalk.discussions.events.{ ChannelCommandEvent, ChannelEvent, DiscussionEvent, DiscussionsCommandEvent }
import io.branchtalk.shared.infrastructure.Projector
import io.branchtalk.shared.model.UUID
-import io.scalaland.chimney.dsl._
+import io.scalaland.chimney.dsl.*
final class ChannelCommandHandler[F[_]: Sync] extends Projector[F, DiscussionsCommandEvent, (UUID, DiscussionEvent)] {
@@ -28,14 +28,14 @@ final class ChannelCommandHandler[F[_]: Sync] extends Projector[F, DiscussionsCo
}
def toCreate(command: ChannelCommandEvent.Create): F[(UUID, ChannelEvent.Created)] =
- (command.id.uuid -> command.transformInto[ChannelEvent.Created]).pure[F]
+ (command.id.unwrap -> command.transformInto[ChannelEvent.Created]).pure[F]
def toUpdate(command: ChannelCommandEvent.Update): F[(UUID, ChannelEvent.Updated)] =
- (command.id.uuid -> command.transformInto[ChannelEvent.Updated]).pure[F]
+ (command.id.unwrap -> command.transformInto[ChannelEvent.Updated]).pure[F]
def toDelete(command: ChannelCommandEvent.Delete): F[(UUID, ChannelEvent.Deleted)] =
- (command.id.uuid -> command.transformInto[ChannelEvent.Deleted]).pure[F]
+ (command.id.unwrap -> command.transformInto[ChannelEvent.Deleted]).pure[F]
def toRestore(command: ChannelCommandEvent.Restore): F[(UUID, ChannelEvent.Restored)] =
- (command.id.uuid -> command.transformInto[ChannelEvent.Restored]).pure[F]
+ (command.id.unwrap -> command.transformInto[ChannelEvent.Restored]).pure[F]
}
diff --git a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/ChannelPostgresProjector.scala b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/ChannelPostgresProjector.scala
index f7608237..7939c33f 100644
--- a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/ChannelPostgresProjector.scala
+++ b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/ChannelPostgresProjector.scala
@@ -6,7 +6,7 @@ import com.typesafe.scalalogging.Logger
import fs2.Stream
import io.branchtalk.discussions.events.{ ChannelEvent, DiscussionEvent }
import io.branchtalk.logging.MDC
-import io.branchtalk.shared.infrastructure.DoobieSupport._
+import io.branchtalk.shared.infrastructure.DoobieSupport.{ *, given }
import io.branchtalk.shared.infrastructure.Projector
import io.branchtalk.shared.model.UUID
@@ -15,8 +15,6 @@ final class ChannelPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
private val logger = Logger(getClass)
- implicit private val logHandler: LogHandler = doobieLogger(getClass)
-
override def apply(in: Stream[F, DiscussionEvent]): Stream[F, (UUID, DiscussionEvent)] =
in.collect { case DiscussionEvent.ForChannel(event) =>
event
@@ -48,7 +46,7 @@ final class ChannelPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
| ${event.description},
| ${event.createdAt}
|)
- |ON CONFLICT (id) DO NOTHING""".stripMargin.update.run.as(event.id.uuid -> event).transact(transactor)
+ |ON CONFLICT (id) DO NOTHING""".stripMargin.update.run.as(event.id.unwrap -> event).transact(transactor)
}
def toUpdate(event: ChannelEvent.Updated): F[(UUID, ChannelEvent.Updated)] =
@@ -69,21 +67,21 @@ final class ChannelPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
(updates :+ fr"last_modified_at = ${event.modifiedAt}").intercalate(fr",") ++
fr"WHERE id = ${event.id}").update.run.void
)
- .as(event.id.uuid -> event)
+ .as(event.id.unwrap -> event)
.transact(transactor)
}
def toDelete(event: ChannelEvent.Deleted): F[(UUID, ChannelEvent.Deleted)] =
withCorrelationID(event.correlationID) {
sql"UPDATE channels SET deleted = TRUE WHERE id = ${event.id}".update.run
- .as(event.id.uuid -> event)
+ .as(event.id.unwrap -> event)
.transact(transactor)
}
def toRestore(event: ChannelEvent.Restored): F[(UUID, ChannelEvent.Restored)] =
withCorrelationID(event.correlationID) {
sql"UPDATE channels SET deleted = FALSE WHERE id = ${event.id}".update.run
- .as(event.id.uuid -> event)
+ .as(event.id.unwrap -> event)
.transact(transactor)
}
}
diff --git a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/ChannelWritesImpl.scala b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/ChannelWritesImpl.scala
index 7b4de688..257c2405 100644
--- a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/ChannelWritesImpl.scala
+++ b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/ChannelWritesImpl.scala
@@ -4,18 +4,17 @@ import cats.effect.Sync
import io.branchtalk.discussions.events.{ ChannelCommandEvent, DiscussionsCommandEvent }
import io.branchtalk.discussions.model.Channel
import io.branchtalk.logging.{ CorrelationID, MDC }
-import io.branchtalk.shared.infrastructure.{ EventBusProducer, Writes }
-import io.branchtalk.shared.infrastructure.DoobieSupport._
-import io.branchtalk.shared.model._
-import io.scalaland.chimney.dsl._
+import io.branchtalk.shared.infrastructure.{ KafkaEventBus, Writes }
+import io.branchtalk.shared.infrastructure.DoobieSupport.{ *, given }
+import io.branchtalk.shared.model.*
+import io.scalaland.chimney.dsl.*
final class ChannelWritesImpl[F[_]: Sync: MDC](
- producer: EventBusProducer[F, DiscussionsCommandEvent],
+ producer: KafkaEventBus.Producer[F, DiscussionsCommandEvent],
transactor: Transactor[F]
-)(implicit
- uuidGenerator: UUIDGenerator
-) extends Writes[F, Channel, DiscussionsCommandEvent](producer)
- with ChannelWrites[F] {
+)(using UUID.Generator)
+ extends Writes[F, Channel, DiscussionsCommandEvent](producer),
+ ChannelWrites[F] {
private val channelCheck = new EntityCheck("Channel", transactor)
@@ -36,7 +35,7 @@ final class ChannelWritesImpl[F[_]: Sync: MDC](
override def updateChannel(updatedChannel: Channel.Update): F[UpdateScheduled[Channel]] =
for {
id <- updatedChannel.id.pure[F]
- _ <- channelCheck(id, sql"""SELECT 1 FROM channels WHERE id = ${id} AND deleted = FALSE""")
+ _ <- channelCheck(id, sql"""SELECT 1 FROM channels WHERE id = $id AND deleted = FALSE""")
now <- ModificationTime.now[F]
correlationID <- CorrelationID.getCurrentOrGenerate[F]
command = updatedChannel
@@ -51,7 +50,7 @@ final class ChannelWritesImpl[F[_]: Sync: MDC](
for {
correlationID <- CorrelationID.getCurrentOrGenerate[F]
id = deletedChannel.id
- _ <- channelCheck(id, sql"""SELECT 1 FROM channels WHERE id = ${id} AND deleted = FALSE""")
+ _ <- channelCheck(id, sql"""SELECT 1 FROM channels WHERE id = $id AND deleted = FALSE""")
command = deletedChannel.into[ChannelCommandEvent.Delete].withFieldConst(_.correlationID, correlationID).transform
_ <- postEvent(id, DiscussionsCommandEvent.ForChannel(command))
} yield DeletionScheduled(id)
@@ -60,7 +59,7 @@ final class ChannelWritesImpl[F[_]: Sync: MDC](
for {
correlationID <- CorrelationID.getCurrentOrGenerate[F]
id = restoredChannel.id
- _ <- channelCheck(id, sql"""SELECT 1 FROM channels WHERE id = ${id} AND deleted = TRUE""")
+ _ <- channelCheck(id, sql"""SELECT 1 FROM channels WHERE id = $id AND deleted = TRUE""")
command = restoredChannel
.into[ChannelCommandEvent.Restore]
.withFieldConst(_.correlationID, correlationID)
diff --git a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/CommentCommandHandler.scala b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/CommentCommandHandler.scala
index 499b1b6f..1e0c2361 100644
--- a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/CommentCommandHandler.scala
+++ b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/CommentCommandHandler.scala
@@ -6,7 +6,7 @@ import fs2.Stream
import io.branchtalk.discussions.events.{ CommentCommandEvent, CommentEvent, DiscussionEvent, DiscussionsCommandEvent }
import io.branchtalk.shared.infrastructure.Projector
import io.branchtalk.shared.model.UUID
-import io.scalaland.chimney.dsl._
+import io.scalaland.chimney.dsl.*
final class CommentCommandHandler[F[_]: Sync] extends Projector[F, DiscussionsCommandEvent, (UUID, DiscussionEvent)] {
@@ -31,23 +31,23 @@ final class CommentCommandHandler[F[_]: Sync] extends Projector[F, DiscussionsCo
}
def toCreate(command: CommentCommandEvent.Create): F[(UUID, CommentEvent.Created)] =
- (command.id.uuid -> command.transformInto[CommentEvent.Created]).pure[F]
+ (command.id.unwrap -> command.transformInto[CommentEvent.Created]).pure[F]
def toUpdate(command: CommentCommandEvent.Update): F[(UUID, CommentEvent.Updated)] =
- (command.id.uuid -> command.transformInto[CommentEvent.Updated]).pure[F]
+ (command.id.unwrap -> command.transformInto[CommentEvent.Updated]).pure[F]
def toDelete(command: CommentCommandEvent.Delete): F[(UUID, CommentEvent.Deleted)] =
- (command.id.uuid -> command.transformInto[CommentEvent.Deleted]).pure[F]
+ (command.id.unwrap -> command.transformInto[CommentEvent.Deleted]).pure[F]
def toRestore(command: CommentCommandEvent.Restore): F[(UUID, CommentEvent.Restored)] =
- (command.id.uuid -> command.transformInto[CommentEvent.Restored]).pure[F]
+ (command.id.unwrap -> command.transformInto[CommentEvent.Restored]).pure[F]
def toUpvote(command: CommentCommandEvent.Upvote): F[(UUID, CommentEvent.Upvoted)] =
- (command.id.uuid -> command.transformInto[CommentEvent.Upvoted]).pure[F]
+ (command.id.unwrap -> command.transformInto[CommentEvent.Upvoted]).pure[F]
def toDownvote(command: CommentCommandEvent.Downvote): F[(UUID, CommentEvent.Downvoted)] =
- (command.id.uuid -> command.transformInto[CommentEvent.Downvoted]).pure[F]
+ (command.id.unwrap -> command.transformInto[CommentEvent.Downvoted]).pure[F]
def toRevokeVote(command: CommentCommandEvent.RevokeVote): F[(UUID, CommentEvent.VoteRevoked)] =
- (command.id.uuid -> command.transformInto[CommentEvent.VoteRevoked]).pure[F]
+ (command.id.unwrap -> command.transformInto[CommentEvent.VoteRevoked]).pure[F]
}
diff --git a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/CommentPostgresProjector.scala b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/CommentPostgresProjector.scala
index af55c891..8734be4a 100644
--- a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/CommentPostgresProjector.scala
+++ b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/CommentPostgresProjector.scala
@@ -6,10 +6,10 @@ import com.typesafe.scalalogging.Logger
import doobie.Transactor
import fs2.Stream
import io.branchtalk.discussions.events.{ CommentEvent, DiscussionEvent }
-import io.branchtalk.discussions.infrastructure.DoobieExtensions._
+import io.branchtalk.discussions.infrastructure.DoobieExtensions.{ *, given }
import io.branchtalk.discussions.model.{ Comment, User, Vote }
import io.branchtalk.logging.MDC
-import io.branchtalk.shared.infrastructure.DoobieSupport._
+import io.branchtalk.shared.infrastructure.DoobieSupport.{ *, given }
import io.branchtalk.shared.infrastructure.Projector
import io.branchtalk.shared.model.{ ID, UUID }
@@ -18,8 +18,6 @@ final class CommentPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
private val logger = Logger(getClass)
- implicit private val logHandler: LogHandler = doobieLogger(getClass)
-
override def apply(in: Stream[F, DiscussionEvent]): Stream[F, (UUID, DiscussionEvent)] =
in.collect { case DiscussionEvent.ForComment(event) =>
event
@@ -44,7 +42,10 @@ final class CommentPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
.fold(0.pure[ConnectionIO]) { replyId =>
sql"""SELECT nesting_level + 1
|FROM comments
- |WHERE id = ${replyId}""".stripMargin.query[Int].option.map(_.getOrElse(0))
+ |WHERE id = $replyId""".stripMargin
+ .queryWithLabel[Int](show"Get nesting level new Discussions' Comment ID=$replyId")
+ .option
+ .map(_.getOrElse(0))
}
.flatMap { nestingLevel =>
sql"""INSERT INTO comments (
@@ -77,7 +78,7 @@ final class CommentPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
|WHERE id = ${event.replyTo}
|""".stripMargin.update.run
}
- .as(event.id.uuid -> event)
+ .as(event.id.unwrap -> event)
.transact(transactor)
}
@@ -96,7 +97,7 @@ final class CommentPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
Sync[ConnectionIO].delay(
logger.warn(show"Comment update ignored as it doesn't contain any modification:\n$event")
)
- }).as(event.id.uuid -> event).transact(transactor)
+ }).as(event.id.unwrap -> event).transact(transactor)
}
def toDelete(event: CommentEvent.Deleted): F[(UUID, CommentEvent.Deleted)] =
@@ -111,7 +112,7 @@ final class CommentPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
|SET replies_nr = replies_nr - 1
|FROM (SELECT reply_to FROM comments WHERE id = ${event.id}) as subquery
|WHERE id = subquery.reply_to
- |""".stripMargin.update.run).as(event.id.uuid -> event).transact(transactor)
+ |""".stripMargin.update.run).as(event.id.unwrap -> event).transact(transactor)
}
def toRestore(event: CommentEvent.Restored): F[(UUID, CommentEvent.Restored)] =
@@ -126,17 +127,17 @@ final class CommentPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
|SET replies_nr = replies_nr + 1
|FROM (SELECT reply_to FROM comments WHERE id = ${event.id}) as subquery
|WHERE id = subquery.reply_to
- |""".stripMargin.update.run).as(event.id.uuid -> event).transact(transactor)
+ |""".stripMargin.update.run).as(event.id.unwrap -> event).transact(transactor)
}
private def fetchVote(commentID: ID[Comment], voterID: ID[User]) =
- sql"SELECT vote FROM comment_votes WHERE comment_id = ${commentID} AND voter_id = ${voterID}"
- .query[Vote.Type]
+ sql"SELECT vote FROM comment_votes WHERE comment_id = $commentID AND voter_id = $voterID"
+ .queryWithLabel[Vote.Type](show"Get Discussions' Vote for Comment=$commentID AND Voter=$voterID")
.option
private def updateCommentVotes(commentID: ID[Comment], upvotes: Fragment, downvotes: Fragment) =
(fr"WITH nw AS (SELECT" ++ upvotes ++ fr"AS upvotes,"
- ++ downvotes ++ fr"AS downvotes FROM comments WHERE id = ${commentID})" ++
+ ++ downvotes ++ fr"AS downvotes FROM comments WHERE id = $commentID)" ++
fr"""
|UPDATE comments
|SET upvotes_nr = nw.upvotes,
@@ -144,7 +145,7 @@ final class CommentPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
| total_score = nw.upvotes - nw.downvotes,
| controversial_score = LEAST(nw.upvotes, nw.downvotes)
|FROM nw
- |WHERE id = ${commentID}""".stripMargin).update.run
+ |WHERE id = $commentID""".stripMargin).update.run
def toUpvote(event: CommentEvent.Upvoted): F[(UUID, CommentEvent.Upvoted)] =
withCorrelationID(event.correlationID) {
@@ -173,7 +174,7 @@ final class CommentPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
|)""".stripMargin.update.run >>
updateCommentVotes(event.id, fr"upvotes_nr + 1", fr"downvotes_nr").void
}
- .as(event.id.uuid -> event)
+ .as(event.id.unwrap -> event)
.transact(transactor)
}
@@ -204,7 +205,7 @@ final class CommentPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
|)""".stripMargin.update.run >>
updateCommentVotes(event.id, fr"upvotes_nr", fr"downvotes_nr + 1").void
}
- .as(event.id.uuid -> event)
+ .as(event.id.unwrap -> event)
.transact(transactor)
}
@@ -228,7 +229,7 @@ final class CommentPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
// do nothing - vote doesn't exist
().pure[ConnectionIO]
}
- .as(event.id.uuid -> event)
+ .as(event.id.unwrap -> event)
.transact(transactor)
}
}
diff --git a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/CommentWritesImpl.scala b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/CommentWritesImpl.scala
index 45eca77b..722b2451 100644
--- a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/CommentWritesImpl.scala
+++ b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/CommentWritesImpl.scala
@@ -4,18 +4,17 @@ import cats.effect.Sync
import io.branchtalk.discussions.events.{ CommentCommandEvent, DiscussionsCommandEvent }
import io.branchtalk.discussions.model.{ Channel, Comment, Post }
import io.branchtalk.logging.{ CorrelationID, MDC }
-import io.branchtalk.shared.infrastructure.{ EventBusProducer, Writes }
-import io.branchtalk.shared.infrastructure.DoobieSupport._
-import io.branchtalk.shared.model._
-import io.scalaland.chimney.dsl._
+import io.branchtalk.shared.infrastructure.{ KafkaEventBus, Writes }
+import io.branchtalk.shared.infrastructure.DoobieSupport.{ *, given }
+import io.branchtalk.shared.model.*
+import io.scalaland.chimney.dsl.*
final class CommentWritesImpl[F[_]: Sync: MDC](
- producer: EventBusProducer[F, DiscussionsCommandEvent],
+ producer: KafkaEventBus.Producer[F, DiscussionsCommandEvent],
transactor: Transactor[F]
-)(implicit
- uuidGenerator: UUIDGenerator
-) extends Writes[F, Comment, DiscussionsCommandEvent](producer)
- with CommentWrites[F] {
+)(using UUID.Generator)
+ extends Writes[F, Comment, DiscussionsCommandEvent](producer),
+ CommentWrites[F] {
private val postCheck = new ParentCheck[Post]("Post", transactor)
private val commentCheck = new EntityCheck("Comment", transactor)
@@ -43,7 +42,7 @@ final class CommentWritesImpl[F[_]: Sync: MDC](
for {
correlationID <- CorrelationID.getCurrentOrGenerate[F]
id = updatedComment.id
- _ <- commentCheck(id, sql"""SELECT 1 FROM comments WHERE id = ${id} AND deleted = FALSE""")
+ _ <- commentCheck(id, sql"""SELECT 1 FROM comments WHERE id = $id AND deleted = FALSE""")
now <- ModificationTime.now[F]
command = updatedComment
.into[CommentCommandEvent.Update]
@@ -57,7 +56,7 @@ final class CommentWritesImpl[F[_]: Sync: MDC](
for {
correlationID <- CorrelationID.getCurrentOrGenerate[F]
id = deletedComment.id
- _ <- commentCheck(id, sql"""SELECT 1 FROM comments WHERE id = ${id} AND deleted = FALSE""")
+ _ <- commentCheck(id, sql"""SELECT 1 FROM comments WHERE id = $id AND deleted = FALSE""")
command = deletedComment.into[CommentCommandEvent.Delete].withFieldConst(_.correlationID, correlationID).transform
_ <- postEvent(id, DiscussionsCommandEvent.ForComment(command))
} yield DeletionScheduled(id)
@@ -66,7 +65,7 @@ final class CommentWritesImpl[F[_]: Sync: MDC](
for {
correlationID <- CorrelationID.getCurrentOrGenerate[F]
id = restoredComment.id
- _ <- commentCheck(id, sql"""SELECT 1 FROM comments WHERE id = ${id} AND deleted = TRUE""")
+ _ <- commentCheck(id, sql"""SELECT 1 FROM comments WHERE id = $id AND deleted = TRUE""")
command = restoredComment
.into[CommentCommandEvent.Restore]
.withFieldConst(_.correlationID, correlationID)
@@ -78,7 +77,7 @@ final class CommentWritesImpl[F[_]: Sync: MDC](
for {
correlationID <- CorrelationID.getCurrentOrGenerate[F]
id = vote.id
- _ <- commentCheck(id, sql"""SELECT 1 FROM comments WHERE id = ${id} AND deleted = FALSE""")
+ _ <- commentCheck(id, sql"""SELECT 1 FROM comments WHERE id = $id AND deleted = FALSE""")
command = vote.into[CommentCommandEvent.Upvote].withFieldConst(_.correlationID, correlationID).transform
_ <- postEvent(id, DiscussionsCommandEvent.ForComment(command))
} yield ()
@@ -87,7 +86,7 @@ final class CommentWritesImpl[F[_]: Sync: MDC](
for {
correlationID <- CorrelationID.getCurrentOrGenerate[F]
id = vote.id
- _ <- commentCheck(id, sql"""SELECT 1 FROM comments WHERE id = ${id} AND deleted = FALSE""")
+ _ <- commentCheck(id, sql"""SELECT 1 FROM comments WHERE id = $id AND deleted = FALSE""")
command = vote.into[CommentCommandEvent.Downvote].withFieldConst(_.correlationID, correlationID).transform
_ <- postEvent(id, DiscussionsCommandEvent.ForComment(command))
} yield ()
@@ -96,7 +95,7 @@ final class CommentWritesImpl[F[_]: Sync: MDC](
for {
correlationID <- CorrelationID.getCurrentOrGenerate[F]
id = vote.id
- _ <- commentCheck(id, sql"""SELECT 1 FROM comments WHERE id = ${id} AND deleted = FALSE""")
+ _ <- commentCheck(id, sql"""SELECT 1 FROM comments WHERE id = $id AND deleted = FALSE""")
command = vote.into[CommentCommandEvent.RevokeVote].withFieldConst(_.correlationID, correlationID).transform
_ <- postEvent(id, DiscussionsCommandEvent.ForComment(command))
} yield ()
diff --git a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/PostCommandHandler.scala b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/PostCommandHandler.scala
index e5622cbf..fa1fd490 100644
--- a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/PostCommandHandler.scala
+++ b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/PostCommandHandler.scala
@@ -6,7 +6,7 @@ import fs2.Stream
import io.branchtalk.discussions.events.{ DiscussionEvent, DiscussionsCommandEvent, PostCommandEvent, PostEvent }
import io.branchtalk.shared.infrastructure.Projector
import io.branchtalk.shared.model.UUID
-import io.scalaland.chimney.dsl._
+import io.scalaland.chimney.dsl.*
final class PostCommandHandler[F[_]: Sync] extends Projector[F, DiscussionsCommandEvent, (UUID, DiscussionEvent)] {
@@ -31,23 +31,23 @@ final class PostCommandHandler[F[_]: Sync] extends Projector[F, DiscussionsComma
}
def toCreate(command: PostCommandEvent.Create): F[(UUID, PostEvent.Created)] =
- (command.id.uuid -> command.transformInto[PostEvent.Created]).pure[F]
+ (command.id.unwrap -> command.transformInto[PostEvent.Created]).pure[F]
def toUpdate(command: PostCommandEvent.Update): F[(UUID, PostEvent.Updated)] =
- (command.id.uuid -> command.transformInto[PostEvent.Updated]).pure[F]
+ (command.id.unwrap -> command.transformInto[PostEvent.Updated]).pure[F]
def toDelete(command: PostCommandEvent.Delete): F[(UUID, PostEvent.Deleted)] =
- (command.id.uuid -> command.transformInto[PostEvent.Deleted]).pure[F]
+ (command.id.unwrap -> command.transformInto[PostEvent.Deleted]).pure[F]
def toRestore(command: PostCommandEvent.Restore): F[(UUID, PostEvent.Restored)] =
- (command.id.uuid -> command.transformInto[PostEvent.Restored]).pure[F]
+ (command.id.unwrap -> command.transformInto[PostEvent.Restored]).pure[F]
def toUpvote(command: PostCommandEvent.Upvote): F[(UUID, PostEvent.Upvoted)] =
- (command.id.uuid -> command.transformInto[PostEvent.Upvoted]).pure[F]
+ (command.id.unwrap -> command.transformInto[PostEvent.Upvoted]).pure[F]
def toDownvote(command: PostCommandEvent.Downvote): F[(UUID, PostEvent.Downvoted)] =
- (command.id.uuid -> command.transformInto[PostEvent.Downvoted]).pure[F]
+ (command.id.unwrap -> command.transformInto[PostEvent.Downvoted]).pure[F]
def toRevokeVote(command: PostCommandEvent.RevokeVote): F[(UUID, PostEvent.VoteRevoked)] =
- (command.id.uuid -> command.transformInto[PostEvent.VoteRevoked]).pure[F]
+ (command.id.unwrap -> command.transformInto[PostEvent.VoteRevoked]).pure[F]
}
diff --git a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/PostPostgresProjector.scala b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/PostPostgresProjector.scala
index 77ad623d..217e74a4 100644
--- a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/PostPostgresProjector.scala
+++ b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/PostPostgresProjector.scala
@@ -6,10 +6,10 @@ import com.typesafe.scalalogging.Logger
import doobie.Transactor
import fs2.Stream
import io.branchtalk.discussions.events.{ DiscussionEvent, PostEvent }
-import io.branchtalk.discussions.infrastructure.DoobieExtensions._
+import io.branchtalk.discussions.infrastructure.DoobieExtensions.{ *, given }
import io.branchtalk.discussions.model.{ Post, User, Vote }
import io.branchtalk.logging.MDC
-import io.branchtalk.shared.infrastructure.DoobieSupport._
+import io.branchtalk.shared.infrastructure.DoobieSupport.{ *, given }
import io.branchtalk.shared.infrastructure.Projector
import io.branchtalk.shared.model.{ ID, UUID }
@@ -18,8 +18,6 @@ final class PostPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
private val logger = Logger(getClass)
- implicit private val logHandler: LogHandler = doobieLogger(getClass)
-
override def apply(in: Stream[F, DiscussionEvent]): Stream[F, (UUID, DiscussionEvent)] =
in.collect { case DiscussionEvent.ForPost(event) =>
event
@@ -61,7 +59,7 @@ final class PostPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
| ${contentRaw},
| ${event.createdAt}
|)
- |ON CONFLICT (id) DO NOTHING""".stripMargin.update.run.as(event.id.uuid -> event).transact(transactor)
+ |ON CONFLICT (id) DO NOTHING""".stripMargin.update.run.as(event.id.unwrap -> event).transact(transactor)
}
def toUpdate(event: PostEvent.Updated): F[(UUID, PostEvent.Updated)] =
@@ -83,29 +81,31 @@ final class PostPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
Sync[ConnectionIO].delay(
logger.warn(show"Post update ignored as it doesn't contain any modification:\n$event")
)
- }).as(event.id.uuid -> event).transact(transactor)
+ }).as(event.id.unwrap -> event).transact(transactor)
}
def toDelete(event: PostEvent.Deleted): F[(UUID, PostEvent.Deleted)] =
withCorrelationID(event.correlationID) {
sql"UPDATE posts SET deleted = TRUE WHERE id = ${event.id}".update.run
- .as(event.id.uuid -> event)
+ .as(event.id.unwrap -> event)
.transact(transactor)
}
def toRestore(event: PostEvent.Restored): F[(UUID, PostEvent.Restored)] =
withCorrelationID(event.correlationID) {
sql"UPDATE posts SET deleted = FALSE WHERE id = ${event.id}".update.run
- .as(event.id.uuid -> event)
+ .as(event.id.unwrap -> event)
.transact(transactor)
}
private def fetchVote(postID: ID[Post], voterID: ID[User]) =
- sql"SELECT vote FROM post_votes WHERE post_id = ${postID} AND voter_id = ${voterID}".query[Vote.Type].option
+ sql"SELECT vote FROM post_votes WHERE post_id = $postID AND voter_id = $voterID"
+ .queryWithLabel[Vote.Type](show"Get Discussions' Vote for Post=$postID AND Voter=$voterID")
+ .option
private def updatePostVotes(postID: ID[Post], upvotes: Fragment, downvotes: Fragment) =
(fr"WITH nw AS (SELECT" ++ upvotes ++ fr"AS upvotes,"
- ++ downvotes ++ fr"AS downvotes FROM posts WHERE id = ${postID})" ++
+ ++ downvotes ++ fr"AS downvotes FROM posts WHERE id = $postID)" ++
fr"""
|UPDATE posts
|SET upvotes_nr = nw.upvotes,
@@ -113,7 +113,7 @@ final class PostPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
| total_score = nw.upvotes - nw.downvotes,
| controversial_score = LEAST(nw.upvotes, nw.downvotes)
|FROM nw
- |WHERE id = ${postID}""".stripMargin).update.run
+ |WHERE id = $postID""".stripMargin).update.run
def toUpvote(event: PostEvent.Upvoted): F[(UUID, PostEvent.Upvoted)] =
withCorrelationID(event.correlationID) {
@@ -142,7 +142,7 @@ final class PostPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
|)""".stripMargin.update.run >>
updatePostVotes(event.id, fr"upvotes_nr + 1", fr"downvotes_nr").void
}
- .as(event.id.uuid -> event)
+ .as(event.id.unwrap -> event)
.transact(transactor)
}
@@ -173,7 +173,7 @@ final class PostPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
|)""".stripMargin.update.run >>
updatePostVotes(event.id, fr"upvotes_nr", fr"downvotes_nr + 1").void
}
- .as(event.id.uuid -> event)
+ .as(event.id.unwrap -> event)
.transact(transactor)
}
@@ -197,7 +197,7 @@ final class PostPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
// do nothing - vote doesn't exist
().pure[ConnectionIO]
}
- .as(event.id.uuid -> event)
+ .as(event.id.unwrap -> event)
.transact(transactor)
}
}
diff --git a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/PostWritesImpl.scala b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/PostWritesImpl.scala
index b876e135..6e79773b 100644
--- a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/PostWritesImpl.scala
+++ b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/PostWritesImpl.scala
@@ -1,32 +1,26 @@
package io.branchtalk.discussions.writes
import cats.effect.Sync
-import eu.timepit.refined.collection.NonEmpty
-import eu.timepit.refined.types.string.NonEmptyString
import io.branchtalk.discussions.events.{ DiscussionsCommandEvent, PostCommandEvent }
import io.branchtalk.discussions.model.{ Channel, Post }
import io.branchtalk.logging.{ CorrelationID, MDC }
-import io.branchtalk.shared.infrastructure.{ EventBusProducer, NormalizeForUrl, Writes }
-import io.branchtalk.shared.infrastructure.DoobieSupport._
-import io.branchtalk.shared.model._
-import io.scalaland.chimney.dsl._
+import io.branchtalk.shared.infrastructure.{ KafkaEventBus, NormalizeForUrl, Writes }
+import io.branchtalk.shared.infrastructure.DoobieSupport.{ *, given }
+import io.branchtalk.shared.model.*
+import io.scalaland.chimney.dsl.*
final class PostWritesImpl[F[_]: Sync: MDC](
- producer: EventBusProducer[F, DiscussionsCommandEvent],
+ producer: KafkaEventBus.Producer[F, DiscussionsCommandEvent],
transactor: Transactor[F]
-)(implicit
- uuidGenerator: UUIDGenerator
-) extends Writes[F, Post, DiscussionsCommandEvent](producer)
- with PostWrites[F] {
+)(using UUID.Generator)
+ extends Writes[F, Post, DiscussionsCommandEvent](producer),
+ PostWrites[F] {
private val channelCheck = new ParentCheck[Channel]("Channel", transactor)
private val postCheck = new EntityCheck("Post", transactor)
private def titleToUrlTitle(title: Post.Title): F[Post.UrlTitle] =
- ParseRefined[F]
- .parse[NonEmpty](NormalizeForUrl(title.nonEmptyString.value))
- .handleError(_ => "post": NonEmptyString)
- .map(Post.UrlTitle(_))
+ ParseNewtype[F].parse[Post.UrlTitle](NormalizeForUrl(title.unwrap))
override def createPost(newPost: Post.Create): F[CreationScheduled[Post]] =
for {
@@ -51,7 +45,7 @@ final class PostWritesImpl[F[_]: Sync: MDC](
for {
correlationID <- CorrelationID.getCurrentOrGenerate[F]
id = updatedPost.id
- _ <- postCheck(id, sql"""SELECT 1 FROM posts WHERE id = ${id} AND deleted = FALSE""")
+ _ <- postCheck(id, sql"""SELECT 1 FROM posts WHERE id = $id AND deleted = FALSE""")
now <- ModificationTime.now[F]
newUrlTitle <- updatedPost.newTitle.traverse(titleToUrlTitle)
command = updatedPost
@@ -67,7 +61,7 @@ final class PostWritesImpl[F[_]: Sync: MDC](
for {
correlationID <- CorrelationID.getCurrentOrGenerate[F]
id = deletedPost.id
- _ <- postCheck(id, sql"""SELECT 1 FROM posts WHERE id = ${id} AND deleted = FALSE""")
+ _ <- postCheck(id, sql"""SELECT 1 FROM posts WHERE id = $id AND deleted = FALSE""")
command = deletedPost.into[PostCommandEvent.Delete].withFieldConst(_.correlationID, correlationID).transform
_ <- postEvent(id, DiscussionsCommandEvent.ForPost(command))
} yield DeletionScheduled(id)
@@ -76,7 +70,7 @@ final class PostWritesImpl[F[_]: Sync: MDC](
for {
correlationID <- CorrelationID.getCurrentOrGenerate[F]
id = restoredPost.id
- _ <- postCheck(id, sql"""SELECT 1 FROM posts WHERE id = ${id} AND deleted = TRUE""")
+ _ <- postCheck(id, sql"""SELECT 1 FROM posts WHERE id = $id AND deleted = TRUE""")
command = restoredPost.into[PostCommandEvent.Restore].withFieldConst(_.correlationID, correlationID).transform
_ <- postEvent(id, DiscussionsCommandEvent.ForPost(command))
} yield RestoreScheduled(id)
@@ -85,7 +79,7 @@ final class PostWritesImpl[F[_]: Sync: MDC](
for {
correlationID <- CorrelationID.getCurrentOrGenerate[F]
id = vote.id
- _ <- postCheck(id, sql"""SELECT 1 FROM posts WHERE id = ${id} AND deleted = FALSE""")
+ _ <- postCheck(id, sql"""SELECT 1 FROM posts WHERE id = $id AND deleted = FALSE""")
command = vote.into[PostCommandEvent.Upvote].withFieldConst(_.correlationID, correlationID).transform
_ <- postEvent(id, DiscussionsCommandEvent.ForPost(command))
} yield ()
@@ -94,7 +88,7 @@ final class PostWritesImpl[F[_]: Sync: MDC](
for {
correlationID <- CorrelationID.getCurrentOrGenerate[F]
id = vote.id
- _ <- postCheck(id, sql"""SELECT 1 FROM posts WHERE id = ${id} AND deleted = FALSE""")
+ _ <- postCheck(id, sql"""SELECT 1 FROM posts WHERE id = $id AND deleted = FALSE""")
command = vote.into[PostCommandEvent.Downvote].withFieldConst(_.correlationID, correlationID).transform
_ <- postEvent(id, DiscussionsCommandEvent.ForPost(command))
} yield ()
@@ -103,7 +97,7 @@ final class PostWritesImpl[F[_]: Sync: MDC](
for {
correlationID <- CorrelationID.getCurrentOrGenerate[F]
id = vote.id
- _ <- postCheck(id, sql"""SELECT 1 FROM posts WHERE id = ${id} AND deleted = FALSE""")
+ _ <- postCheck(id, sql"""SELECT 1 FROM posts WHERE id = $id AND deleted = FALSE""")
command = vote.into[PostCommandEvent.RevokeVote].withFieldConst(_.correlationID, correlationID).transform
_ <- postEvent(id, DiscussionsCommandEvent.ForPost(command))
} yield ()
diff --git a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/SubscriptionCommandHandler.scala b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/SubscriptionCommandHandler.scala
index 7d71870f..6c9e47e5 100644
--- a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/SubscriptionCommandHandler.scala
+++ b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/SubscriptionCommandHandler.scala
@@ -11,7 +11,7 @@ import io.branchtalk.discussions.events.{
}
import io.branchtalk.shared.infrastructure.Projector
import io.branchtalk.shared.model.UUID
-import io.scalaland.chimney.dsl._
+import io.scalaland.chimney.dsl.*
final class SubscriptionCommandHandler[F[_]: Sync]
extends Projector[F, DiscussionsCommandEvent, (UUID, DiscussionEvent)] {
@@ -32,8 +32,8 @@ final class SubscriptionCommandHandler[F[_]: Sync]
}
def toSubscribe(command: SubscriptionCommandEvent.Subscribe): F[(UUID, SubscriptionEvent.Subscribed)] =
- (command.subscriberID.uuid -> command.transformInto[SubscriptionEvent.Subscribed]).pure[F]
+ (command.subscriberID.unwrap -> command.transformInto[SubscriptionEvent.Subscribed]).pure[F]
def toUnsubscribe(command: SubscriptionCommandEvent.Unsubscribe): F[(UUID, SubscriptionEvent.Unsubscribed)] =
- (command.subscriberID.uuid -> command.transformInto[SubscriptionEvent.Unsubscribed]).pure[F]
+ (command.subscriberID.unwrap -> command.transformInto[SubscriptionEvent.Unsubscribed]).pure[F]
}
diff --git a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/SubscriptionPostgresProjector.scala b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/SubscriptionPostgresProjector.scala
index 6e729fd4..727dd757 100644
--- a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/SubscriptionPostgresProjector.scala
+++ b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/SubscriptionPostgresProjector.scala
@@ -5,7 +5,7 @@ import com.typesafe.scalalogging.Logger
import fs2.Stream
import io.branchtalk.discussions.events.{ DiscussionEvent, SubscriptionEvent }
import io.branchtalk.logging.MDC
-import io.branchtalk.shared.infrastructure.DoobieSupport._
+import io.branchtalk.shared.infrastructure.DoobieSupport.{ *, given }
import io.branchtalk.shared.infrastructure.Projector
import io.branchtalk.shared.model.UUID
@@ -14,8 +14,6 @@ final class SubscriptionPostgresProjector[F[_]: Sync: MDC](transactor: Transacto
private val logger = Logger(getClass)
- implicit private val logHandler: LogHandler = doobieLogger(getClass)
-
override def apply(in: Stream[F, DiscussionEvent]): Stream[F, (UUID, DiscussionEvent)] =
in.collect { case DiscussionEvent.ForSubscription(event) =>
event
@@ -42,7 +40,7 @@ final class SubscriptionPostgresProjector[F[_]: Sync: MDC](transactor: Transacto
|ON CONFLICT (subscriber_id) DO
|UPDATE
|SET subscriptions_ids = array_distinct(subscriptions.subscriptions_ids || ${event.subscriptions})""" //
- .stripMargin.update.run.as(event.subscriberID.uuid -> event).transact(transactor)
+ .stripMargin.update.run.as(event.subscriberID.unwrap -> event).transact(transactor)
}
def toUnsubscribe(event: SubscriptionEvent.Unsubscribed): F[(UUID, SubscriptionEvent.Unsubscribed)] =
@@ -50,7 +48,7 @@ final class SubscriptionPostgresProjector[F[_]: Sync: MDC](transactor: Transacto
sql"""UPDATE subscriptions
|SET subscriptions_ids = array_diff(subscriptions.subscriptions_ids, ${event.subscriptions})
|WHERE subscriber_id = ${event.subscriberID}""".stripMargin.update.run
- .as(event.subscriberID.uuid -> event)
+ .as(event.subscriberID.unwrap -> event)
.transact(transactor)
}
}
diff --git a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/SubscriptionWritesImpl.scala b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/SubscriptionWritesImpl.scala
index ef70aee7..56e55e2b 100644
--- a/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/SubscriptionWritesImpl.scala
+++ b/modules/discussions-impl/src/main/scala/io/branchtalk/discussions/writes/SubscriptionWritesImpl.scala
@@ -4,20 +4,17 @@ import cats.effect.Sync
import io.branchtalk.discussions.events.{ DiscussionsCommandEvent, SubscriptionCommandEvent }
import io.branchtalk.discussions.model.{ Subscription, User }
import io.branchtalk.logging.{ CorrelationID, MDC }
-import io.branchtalk.shared.infrastructure.{ EventBusProducer, Writes }
-import io.branchtalk.shared.infrastructure.DoobieSupport._
-import io.branchtalk.shared.model._
-import io.scalaland.chimney.dsl._
+import io.branchtalk.shared.infrastructure.{ KafkaEventBus, Writes }
+import io.branchtalk.shared.infrastructure.DoobieSupport.{ *, given }
+import io.branchtalk.shared.model.*
+import io.scalaland.chimney.dsl.*
final class SubscriptionWritesImpl[F[_]: Sync: MDC](
- producer: EventBusProducer[F, DiscussionsCommandEvent],
+ producer: KafkaEventBus.Producer[F, DiscussionsCommandEvent],
transactor: Transactor[F]
-)(implicit
- uuidGenerator: UUIDGenerator
-) extends Writes[F, User, DiscussionsCommandEvent](producer)
- with SubscriptionWrites[F] {
-
- implicit private val logHandler: LogHandler = doobieLogger(getClass)
+)(using UUID.Generator)
+ extends Writes[F, User, DiscussionsCommandEvent](producer),
+ SubscriptionWrites[F] {
private val commonSelect: Fragment =
fr"""SELECT subscriber_id,
@@ -35,8 +32,8 @@ final class SubscriptionWritesImpl[F[_]: Sync: MDC](
.withFieldConst(_.correlationID, correlationID)
.transform
_ <- postEvent(id, DiscussionsCommandEvent.ForSubscription(command))
- subscription <- (commonSelect ++ fr"WHERE subscriber_id = ${id}")
- .query[Subscription]
+ subscription <- (commonSelect ++ fr"WHERE subscriber_id = $id")
+ .queryWithLabel[Subscription](show"Get Discussions' Subscriber ID=$id")
.option
.map(_.getOrElse(Subscription(id, Set.empty)))
.transact(transactor)
@@ -53,8 +50,8 @@ final class SubscriptionWritesImpl[F[_]: Sync: MDC](
.withFieldConst(_.correlationID, correlationID)
.transform
_ <- postEvent(id, DiscussionsCommandEvent.ForSubscription(command))
- subscription <- (commonSelect ++ fr"WHERE subscriber_id = ${id}")
- .query[Subscription]
+ subscription <- (commonSelect ++ fr"WHERE subscriber_id = $id")
+ .queryWithLabel[Subscription](show"Get Discussions' Subscriber ID=$id")
.option
.map(_.getOrElse(Subscription(id, Set.empty)))
.transact(transactor)
diff --git a/modules/discussions-impl/src/it/resources/discussions-test.conf b/modules/discussions-impl/src/test/resources/discussions-test.conf
similarity index 100%
rename from modules/discussions-impl/src/it/resources/discussions-test.conf
rename to modules/discussions-impl/src/test/resources/discussions-test.conf
diff --git a/modules/discussions-impl/src/it/scala/io/branchtalk/discussions/ChannelPaginationSpec.scala b/modules/discussions-impl/src/test/scala/io/branchtalk/discussions/ChannelPaginationSpec.scala
similarity index 57%
rename from modules/discussions-impl/src/it/scala/io/branchtalk/discussions/ChannelPaginationSpec.scala
rename to modules/discussions-impl/src/test/scala/io/branchtalk/discussions/ChannelPaginationSpec.scala
index 602138ac..6996b287 100644
--- a/modules/discussions-impl/src/it/scala/io/branchtalk/discussions/ChannelPaginationSpec.scala
+++ b/modules/discussions-impl/src/test/scala/io/branchtalk/discussions/ChannelPaginationSpec.scala
@@ -1,15 +1,15 @@
package io.branchtalk.discussions
import io.branchtalk.discussions.model.Channel
-import io.branchtalk.shared.model.TestUUIDGenerator
+import io.branchtalk.shared.model.*
import org.specs2.mutable.Specification
-final class ChannelPaginationSpec extends Specification with DiscussionsIOTest with DiscussionsFixtures {
+final class ChannelPaginationSpec extends Specification, DiscussionsIOTest, DiscussionsFixtures {
// Channel pagination tests cannot be run in parallel to other Channel tests (no parent to filter other tests)
sequential
- implicit protected val uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
+ protected given uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
"Channel pagination" should {
@@ -17,24 +17,32 @@ final class ChannelPaginationSpec extends Specification with DiscussionsIOTest w
for {
// given
editorID <- editorIDCreate
- cleanupIDs <- discussionsReads.channelReads.paginate(Channel.Sorting.Newest, 0L, 10).map(_.entities.map(_.id))
+ cleanupIDs <- discussionsReads.channelReads
+ .paginate(Channel.Sorting.Newest, Paginated.Offset(0L), Paginated.Limit(10))
+ .map(_.entities.map(_.id))
_ <- cleanupIDs.traverse(id => discussionsWrites.channelWrites.deleteChannel(Channel.Delete(id, editorID)))
_ <- cleanupIDs
.traverse(discussionsReads.channelReads.deleted(_))
.assert("Channels should be deleted eventually")(_.forall(identity))
.eventually()
paginatedData <- (0 until 20).toList.traverse(_ => channelCreate)
- paginatedIDs <- paginatedData.traverse(discussionsWrites.channelWrites.createChannel).map(_.map(_.id))
+ paginatedIDs <- paginatedData.traverse(discussionsWrites.channelWrites.createChannel).map(_.map(_.unwrap))
_ <- paginatedIDs.traverse(discussionsReads.channelReads.requireById(_)).eventually()
// when
- pagination <- discussionsReads.channelReads.paginate(Channel.Sorting.Newest, 0L, 10)
- pagination2 <- discussionsReads.channelReads.paginate(Channel.Sorting.Newest, 10L, 10)
+ pagination <- discussionsReads.channelReads.paginate(Channel.Sorting.Newest,
+ Paginated.Offset(0L),
+ Paginated.Limit(10)
+ )
+ pagination2 <- discussionsReads.channelReads.paginate(Channel.Sorting.Newest,
+ Paginated.Offset(10L),
+ Paginated.Limit(10)
+ )
} yield {
// then
pagination.entities must haveSize(10)
- pagination.nextOffset.map(_.value) must beSome(10L)
+ pagination.nextOffset.map(_.unwrap) must beSome(10L)
pagination2.entities must haveSize(10)
- pagination2.nextOffset.map(_.value) must beNone
+ pagination2.nextOffset.map(_.unwrap) must beNone
}
}
@@ -43,26 +51,32 @@ final class ChannelPaginationSpec extends Specification with DiscussionsIOTest w
// given
editorID <- editorIDCreate
cleanupIDs <- discussionsReads.channelReads
- .paginate(Channel.Sorting.Newest, 0L, 1000)
+ .paginate(Channel.Sorting.Newest, Paginated.Offset(0L), Paginated.Limit(1000))
.map(_.entities.map(_.id))
.flatMap(_.traverse(id => discussionsWrites.channelWrites.deleteChannel(Channel.Delete(id, editorID))))
- .map(_.map(_.id))
+ .map(_.map(_.unwrap))
_ <- cleanupIDs
.traverse(discussionsReads.channelReads.deleted(_))
.assert("Channels should be deleted eventually")(_.forall(identity))
.eventually()
paginatedData <- (0 until 20).toList.traverse(_ => channelCreate)
- paginatedIDs <- paginatedData.traverse(discussionsWrites.channelWrites.createChannel).map(_.map(_.id))
+ paginatedIDs <- paginatedData.traverse(discussionsWrites.channelWrites.createChannel).map(_.map(_.unwrap))
_ <- paginatedIDs.traverse(discussionsReads.channelReads.requireById(_)).eventually()
// when
- pagination <- discussionsReads.channelReads.paginate(Channel.Sorting.Alphabetically, 0L, 10)
- pagination2 <- discussionsReads.channelReads.paginate(Channel.Sorting.Alphabetically, 10L, 10)
+ pagination <- discussionsReads.channelReads.paginate(Channel.Sorting.Alphabetically,
+ Paginated.Offset(0L),
+ Paginated.Limit(10)
+ )
+ pagination2 <- discussionsReads.channelReads.paginate(Channel.Sorting.Alphabetically,
+ Paginated.Offset(10L),
+ Paginated.Limit(10)
+ )
} yield {
// then
pagination.entities must haveSize(10)
- pagination.nextOffset.map(_.value) must beSome(10L)
+ pagination.nextOffset.map(_.unwrap) must beSome(10L)
pagination2.entities must haveSize(10)
- pagination2.nextOffset.map(_.value) must beNone
+ pagination2.nextOffset.map(_.unwrap) must beNone
}
}
}
diff --git a/modules/discussions-impl/src/it/scala/io/branchtalk/discussions/ChannelReadsWritesSpec.scala b/modules/discussions-impl/src/test/scala/io/branchtalk/discussions/ChannelReadsWritesSpec.scala
similarity index 91%
rename from modules/discussions-impl/src/it/scala/io/branchtalk/discussions/ChannelReadsWritesSpec.scala
rename to modules/discussions-impl/src/test/scala/io/branchtalk/discussions/ChannelReadsWritesSpec.scala
index be188532..a75f227d 100644
--- a/modules/discussions-impl/src/it/scala/io/branchtalk/discussions/ChannelReadsWritesSpec.scala
+++ b/modules/discussions-impl/src/test/scala/io/branchtalk/discussions/ChannelReadsWritesSpec.scala
@@ -2,12 +2,12 @@ package io.branchtalk.discussions
import cats.effect.IO
import io.branchtalk.discussions.model.Channel
-import io.branchtalk.shared.model.{ ID, OptionUpdatable, TestUUIDGenerator, Updatable }
+import io.branchtalk.shared.model.*
import org.specs2.mutable.Specification
-final class ChannelReadsWritesSpec extends Specification with DiscussionsIOTest with DiscussionsFixtures {
+final class ChannelReadsWritesSpec extends Specification, DiscussionsIOTest, DiscussionsFixtures {
- implicit protected val uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
+ protected given uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
"Channel Reads & Writes" should {
@@ -19,7 +19,7 @@ final class ChannelReadsWritesSpec extends Specification with DiscussionsIOTest
creationData <- (0 until 3).toList.traverse(_ => channelCreate)
// when
toCreate <- creationData.traverse(discussionsWrites.channelWrites.createChannel)
- ids = toCreate.map(_.id)
+ ids = toCreate.map(_.unwrap)
channels <- ids.traverse(discussionsReads.channelReads.requireById(_)).eventually()
channelsOpt <- ids.traverse(discussionsReads.channelReads.getById(_)).eventually()
channelsExist <- ids.traverse(discussionsReads.channelReads.exists).eventually()
@@ -62,7 +62,7 @@ final class ChannelReadsWritesSpec extends Specification with DiscussionsIOTest
editorID <- editorIDCreate
creationData <- (0 until 3).toList.traverse(_ => channelCreate)
toCreate <- creationData.traverse(discussionsWrites.channelWrites.createChannel)
- ids = toCreate.map(_.id)
+ ids = toCreate.map(_.unwrap)
created <- ids.traverse(discussionsReads.channelReads.requireById(_)).eventually()
updateData = created.zipWithIndex.collect {
case (Channel(id, data), 0) =>
@@ -104,13 +104,13 @@ final class ChannelReadsWritesSpec extends Specification with DiscussionsIOTest
.collect {
case ((Channel(_, older), Channel(_, newer)), 0) =>
// set case
- older must_=== newer.copy(lastModifiedAt = None)
+ older === newer.copy(lastModifiedAt = None)
case ((Channel(_, older), Channel(_, newer)), 1) =>
// keep case
- older must_=== newer
+ older === newer
case ((Channel(_, older), Channel(_, newer)), 2) =>
// erase case
- older.copy(description = None) must_=== newer.copy(lastModifiedAt = None)
+ older.copy(description = None) === newer.copy(lastModifiedAt = None)
}
.lastOption
.getOrElse(true must beFalse)
@@ -123,7 +123,7 @@ final class ChannelReadsWritesSpec extends Specification with DiscussionsIOTest
creationData <- (0 until 3).toList.traverse(_ => channelCreate)
// when
toCreate <- creationData.traverse(discussionsWrites.channelWrites.createChannel)
- ids = toCreate.map(_.id)
+ ids = toCreate.map(_.unwrap)
_ <- ids.traverse(discussionsReads.channelReads.requireById(_)).eventually()
_ <- ids.map(Channel.Delete(_, editorID)).traverse(discussionsWrites.channelWrites.deleteChannel)
_ <- ids
diff --git a/modules/discussions-impl/src/it/scala/io/branchtalk/discussions/CommentReadsWritesSpec.scala b/modules/discussions-impl/src/test/scala/io/branchtalk/discussions/CommentReadsWritesSpec.scala
similarity index 67%
rename from modules/discussions-impl/src/it/scala/io/branchtalk/discussions/CommentReadsWritesSpec.scala
rename to modules/discussions-impl/src/test/scala/io/branchtalk/discussions/CommentReadsWritesSpec.scala
index 0d18c47a..f5c40a2e 100644
--- a/modules/discussions-impl/src/it/scala/io/branchtalk/discussions/CommentReadsWritesSpec.scala
+++ b/modules/discussions-impl/src/test/scala/io/branchtalk/discussions/CommentReadsWritesSpec.scala
@@ -2,14 +2,14 @@ package io.branchtalk.discussions
import cats.effect.IO
import io.branchtalk.discussions.model.{ Comment, Post }
-import io.branchtalk.shared.model.{ ID, TestUUIDGenerator, Updatable }
+import io.branchtalk.shared.model.*
import org.specs2.mutable.Specification
import scala.concurrent.duration.DurationInt
-final class CommentReadsWritesSpec extends Specification with DiscussionsIOTest with DiscussionsFixtures {
+final class CommentReadsWritesSpec extends Specification, DiscussionsIOTest, DiscussionsFixtures {
- implicit protected val uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
+ protected given uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
"Comment Reads & Writes" should {
@@ -28,14 +28,14 @@ final class CommentReadsWritesSpec extends Specification with DiscussionsIOTest
"create a Comment and eventually read it" in {
for {
// given
- channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.id)
+ channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.unwrap)
_ <- discussionsReads.channelReads.requireById(channelID).eventually()
- postID <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.id)
+ postID <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.unwrap)
_ <- discussionsReads.postReads.requireById(postID).eventually()
creationData <- (0 until 3).toList.traverse(_ => commentCreate(postID))
// when
toCreate <- creationData.traverse(discussionsWrites.commentWrites.createComment)
- ids = toCreate.map(_.id)
+ ids = toCreate.map(_.unwrap)
comments <- ids.traverse(discussionsReads.commentReads.requireById(_)).eventually()
commentsOpt <- ids.traverse(discussionsReads.commentReads.getById(_)).eventually()
commentsExist <- ids.traverse(discussionsReads.commentReads.exists).eventually()
@@ -52,9 +52,9 @@ final class CommentReadsWritesSpec extends Specification with DiscussionsIOTest
"don't update a Comment that doesn't exists" in {
for {
// given
- channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.id)
+ channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.unwrap)
_ <- discussionsReads.channelReads.requireById(channelID).eventually()
- postID <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.id)
+ postID <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.unwrap)
editorID <- editorIDCreate
creationData <- (0 until 3).toList.traverse(_ => commentCreate(postID))
fakeUpdateData <- creationData.traverse { data =>
@@ -76,14 +76,14 @@ final class CommentReadsWritesSpec extends Specification with DiscussionsIOTest
"update an existing Comment" in {
for {
// given
- channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.id)
+ channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.unwrap)
_ <- discussionsReads.channelReads.requireById(channelID).eventually()
- postID <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.id)
+ postID <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.unwrap)
_ <- discussionsReads.postReads.requireById(postID).eventually()
editorID <- editorIDCreate
creationData <- (0 until 2).toList.traverse(_ => commentCreate(postID))
toCreate <- creationData.traverse(discussionsWrites.commentWrites.createComment)
- ids = toCreate.map(_.id)
+ ids = toCreate.map(_.unwrap)
created <- ids.traverse(discussionsReads.commentReads.requireById(_)).eventually()
updateData = created.zipWithIndex.collect {
case (Comment(id, data), 0) =>
@@ -113,10 +113,10 @@ final class CommentReadsWritesSpec extends Specification with DiscussionsIOTest
.collect {
case ((Comment(_, older), Comment(_, newer)), 0) =>
// set case
- older must_=== newer.copy(lastModifiedAt = None)
+ older === newer.copy(lastModifiedAt = None)
case ((Comment(_, older), Comment(_, newer)), 1) =>
// keep case
- older must_=== newer
+ older === newer
}
.lastOption
.getOrElse(true must beFalse)
@@ -125,15 +125,15 @@ final class CommentReadsWritesSpec extends Specification with DiscussionsIOTest
"allow delete and restore of a created Comment" in {
for {
// given
- channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.id)
+ channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.unwrap)
_ <- discussionsReads.channelReads.requireById(channelID).eventually()
- postID <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.id)
+ postID <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.unwrap)
_ <- discussionsReads.postReads.requireById(postID).eventually()
creationData <- (0 until 3).toList.traverse(_ => commentCreate(postID))
editorID <- editorIDCreate
// when
toCreate <- creationData.traverse(discussionsWrites.commentWrites.createComment)
- ids = toCreate.map(_.id)
+ ids = toCreate.map(_.unwrap)
_ <- ids.traverse(discussionsReads.commentReads.requireById(_)).eventually()
_ <- ids.map(Comment.Delete(_, editorID)).traverse(discussionsWrites.commentWrites.deleteComment)
_ <- ids
@@ -168,13 +168,13 @@ final class CommentReadsWritesSpec extends Specification with DiscussionsIOTest
"handle Upvoting and Downvoting of Comments" in {
for {
// given
- channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.id)
+ channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.unwrap)
_ <- discussionsReads.channelReads.requireById(channelID).eventually()
- postID <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.id)
+ postID <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.unwrap)
_ <- discussionsReads.postReads.requireById(postID).eventually()
creationData <- (0 until 4).toList.traverse(_ => commentCreate(postID))
toCreate <- creationData.traverse(discussionsWrites.commentWrites.createComment)
- ids = toCreate.map(_.id)
+ ids = toCreate.map(_.unwrap)
_ <- ids.traverse(discussionsReads.commentReads.requireById(_)).eventually()
user0ID <- voterIDCreate
user1ID <- voterIDCreate
@@ -187,7 +187,7 @@ final class CommentReadsWritesSpec extends Specification with DiscussionsIOTest
_ <- discussionsWrites.commentWrites.downvoteComment(Comment.Downvote(ids(3), user3ID))
firstVotes <- ids
.traverse(discussionsReads.commentReads.requireById(_))
- .assert("Comments should have first Votes applied")(_.forall(_.data.totalScore.toInt =!= 0))
+ .assert("Comments should have first Votes applied")(_.forall(_.data.totalScore.unwrap =!= 0))
.eventually(delay = 1.second)
_ <- discussionsWrites.commentWrites.downvoteComment(Comment.Downvote(ids(0), user0ID))
_ <- discussionsWrites.commentWrites.revokeCommentVote(Comment.RevokeVote(ids(1), user1ID))
@@ -207,40 +207,40 @@ final class CommentReadsWritesSpec extends Specification with DiscussionsIOTest
thirdsVotes <- ids
.traverse(discussionsReads.commentReads.requireById(_))
.assert("Comments should have third Votes applied")(
- _.forall(p => p.data.totalScore.toInt > 0 || p.data.controversialScore.toNonNegativeInt.toInt > 0)
+ _.forall(p => p.data.totalScore.unwrap > 0 || p.data.controversialScore.unwrap > 0)
)
.eventually(delay = 1.second)
} yield {
// then
- firstVotes.map(_.data.totalScore) must_=== List(Comment.TotalScore(1),
- Comment.TotalScore(1),
- Comment.TotalScore(-1),
- Comment.TotalScore(-1)
+ firstVotes.map(_.data.totalScore) === List(Comment.TotalScore(1),
+ Comment.TotalScore(1),
+ Comment.TotalScore(-1),
+ Comment.TotalScore(-1)
)
- firstVotes.map(_.data.controversialScore) must_=== List(Comment.ControversialScore(0),
- Comment.ControversialScore(0),
- Comment.ControversialScore(0),
- Comment.ControversialScore(0)
+ firstVotes.map(_.data.controversialScore) === List(Comment.ControversialScore(0),
+ Comment.ControversialScore(0),
+ Comment.ControversialScore(0),
+ Comment.ControversialScore(0)
)
- secondsVotes.map(_.data.totalScore) must_=== List(Comment.TotalScore(-1),
- Comment.TotalScore(0),
- Comment.TotalScore(1),
- Comment.TotalScore(0)
+ secondsVotes.map(_.data.totalScore) === List(Comment.TotalScore(-1),
+ Comment.TotalScore(0),
+ Comment.TotalScore(1),
+ Comment.TotalScore(0)
)
- secondsVotes.map(_.data.controversialScore) must_=== List(Comment.ControversialScore(0),
- Comment.ControversialScore(0),
- Comment.ControversialScore(0),
- Comment.ControversialScore(0)
+ secondsVotes.map(_.data.controversialScore) === List(Comment.ControversialScore(0),
+ Comment.ControversialScore(0),
+ Comment.ControversialScore(0),
+ Comment.ControversialScore(0)
)
- thirdsVotes.map(_.data.totalScore) must_=== List(Comment.TotalScore(0),
- Comment.TotalScore(1),
- Comment.TotalScore(2),
- Comment.TotalScore(1)
+ thirdsVotes.map(_.data.totalScore) === List(Comment.TotalScore(0),
+ Comment.TotalScore(1),
+ Comment.TotalScore(2),
+ Comment.TotalScore(1)
)
- thirdsVotes.map(_.data.controversialScore) must_=== List(Comment.ControversialScore(1),
- Comment.ControversialScore(0),
- Comment.ControversialScore(0),
- Comment.ControversialScore(0)
+ thirdsVotes.map(_.data.controversialScore) === List(Comment.ControversialScore(1),
+ Comment.ControversialScore(0),
+ Comment.ControversialScore(0),
+ Comment.ControversialScore(0)
)
}
}
@@ -248,67 +248,87 @@ final class CommentReadsWritesSpec extends Specification with DiscussionsIOTest
"paginate newest Comments by Posts" in {
for {
// given
- channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.id)
+ channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.unwrap)
_ <- discussionsReads.channelReads.requireById(channelID).eventually()
- postID <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.id)
+ postID <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.unwrap)
_ <- discussionsReads.postReads.requireById(postID).eventually()
- post2ID <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.id)
+ post2ID <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.unwrap)
_ <- discussionsReads.postReads.requireById(post2ID).eventually()
paginatedData <- (0 until 20).toList.traverse(_ => commentCreate(postID))
- paginatedIDs <- paginatedData.traverse(discussionsWrites.commentWrites.createComment).map(_.map(_.id))
+ paginatedIDs <- paginatedData.traverse(discussionsWrites.commentWrites.createComment).map(_.map(_.unwrap))
nonPaginatedData <- (0 until 20).toList.traverse(_ => commentCreate(post2ID))
- nonPaginatedIds <- nonPaginatedData.traverse(discussionsWrites.commentWrites.createComment).map(_.map(_.id))
+ nonPaginatedIds <- nonPaginatedData.traverse(discussionsWrites.commentWrites.createComment).map(_.map(_.unwrap))
_ <- (paginatedIDs ++ nonPaginatedIds).traverse(discussionsReads.commentReads.requireById(_)).eventually()
// when
- pagination <- discussionsReads.commentReads.paginate(postID, None, Comment.Sorting.Newest, 0L, 10)
- pagination2 <- discussionsReads.commentReads.paginate(postID, None, Comment.Sorting.Newest, 10L, 10)
+ pagination <- discussionsReads.commentReads.paginate(postID,
+ None,
+ Comment.Sorting.Newest,
+ Paginated.Offset(0L),
+ Paginated.Limit(10)
+ )
+ pagination2 <- discussionsReads.commentReads.paginate(postID,
+ None,
+ Comment.Sorting.Newest,
+ Paginated.Offset(10L),
+ Paginated.Limit(10)
+ )
} yield {
// then
pagination.entities must haveSize(10)
- pagination.nextOffset.map(_.value) must beSome(10L)
+ pagination.nextOffset.map(_.unwrap) must beSome(10L)
pagination2.entities must haveSize(10)
- pagination2.nextOffset.map(_.value) must beNone
+ pagination2.nextOffset.map(_.unwrap) must beNone
}
}
"paginate newest Comments by Replies" in {
for {
// given
- channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.id)
+ channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.unwrap)
_ <- discussionsReads.channelReads.requireById(channelID).eventually()
- postID <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.id)
+ postID <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.unwrap)
_ <- discussionsReads.postReads.requireById(postID).eventually()
- commentID <- commentCreate(postID).flatMap(discussionsWrites.commentWrites.createComment).map(_.id)
+ commentID <- commentCreate(postID).flatMap(discussionsWrites.commentWrites.createComment).map(_.unwrap)
paginatedData <- (0 until 20).toList.traverse(_ => commentCreate(postID).map(_.copy(replyTo = commentID.some)))
- paginatedIDs <- paginatedData.traverse(discussionsWrites.commentWrites.createComment).map(_.map(_.id))
+ paginatedIDs <- paginatedData.traverse(discussionsWrites.commentWrites.createComment).map(_.map(_.unwrap))
nonPaginatedData <- (0 until 20).toList.traverse(_ => commentCreate(postID))
- nonPaginatedIds <- nonPaginatedData.traverse(discussionsWrites.commentWrites.createComment).map(_.map(_.id))
+ nonPaginatedIds <- nonPaginatedData.traverse(discussionsWrites.commentWrites.createComment).map(_.map(_.unwrap))
_ <- (paginatedIDs ++ nonPaginatedIds).traverse(discussionsReads.commentReads.requireById(_)).eventually()
// when
- pagination <- discussionsReads.commentReads.paginate(postID, commentID.some, Comment.Sorting.Newest, 0L, 10)
- pagination2 <- discussionsReads.commentReads.paginate(postID, commentID.some, Comment.Sorting.Newest, 10L, 10)
+ pagination <- discussionsReads.commentReads.paginate(postID,
+ commentID.some,
+ Comment.Sorting.Newest,
+ Paginated.Offset(0L),
+ Paginated.Limit(10)
+ )
+ pagination2 <- discussionsReads.commentReads.paginate(postID,
+ commentID.some,
+ Comment.Sorting.Newest,
+ Paginated.Offset(10L),
+ Paginated.Limit(10)
+ )
} yield {
// then
pagination.entities must haveSize(10)
- pagination.nextOffset.map(_.value) must beSome(10L)
+ pagination.nextOffset.map(_.unwrap) must beSome(10L)
pagination2.entities must haveSize(10)
- pagination2.nextOffset.map(_.value) must beNone
+ pagination2.nextOffset.map(_.unwrap) must beNone
}
}
"paginate hottest Comments by Posts" in {
for {
// given
- channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.id)
+ channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.unwrap)
_ <- discussionsReads.channelReads.requireById(channelID).eventually()
- postID <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.id)
+ postID <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.unwrap)
_ <- discussionsReads.postReads.requireById(postID).eventually()
- post2ID <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.id)
+ post2ID <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.unwrap)
_ <- discussionsReads.postReads.requireById(post2ID).eventually()
paginatedData <- (0 until 20).toList.traverse(_ => commentCreate(postID))
- paginatedIDs <- paginatedData.traverse(discussionsWrites.commentWrites.createComment).map(_.map(_.id))
+ paginatedIDs <- paginatedData.traverse(discussionsWrites.commentWrites.createComment).map(_.map(_.unwrap))
nonPaginatedData <- (0 until 20).toList.traverse(_ => commentCreate(post2ID))
- nonPaginatedIds <- nonPaginatedData.traverse(discussionsWrites.commentWrites.createComment).map(_.map(_.id))
+ nonPaginatedIds <- nonPaginatedData.traverse(discussionsWrites.commentWrites.createComment).map(_.map(_.unwrap))
_ <- (paginatedIDs ++ nonPaginatedIds).traverse(discussionsReads.commentReads.requireById(_)).eventually()
user1ID <- voterIDCreate
user2ID <- voterIDCreate
@@ -319,34 +339,44 @@ final class CommentReadsWritesSpec extends Specification with DiscussionsIOTest
_ <- paginatedIDs
.traverse(discussionsReads.commentReads.requireById(_))
.assert("Votes should be eventually applied")(
- _.forall(e => e.data.totalScore.toInt > 0 || e.data.controversialScore.toNonNegativeInt.value > 0)
+ _.forall(e => e.data.totalScore.unwrap > 0 || e.data.controversialScore.unwrap > 0)
)
.eventually(delay = 1.second)
// when
- pagination <- discussionsReads.commentReads.paginate(postID, None, Comment.Sorting.Hottest, 0L, 10)
- pagination2 <- discussionsReads.commentReads.paginate(postID, None, Comment.Sorting.Hottest, 10L, 10)
+ pagination <- discussionsReads.commentReads.paginate(postID,
+ None,
+ Comment.Sorting.Hottest,
+ Paginated.Offset(0L),
+ Paginated.Limit(10)
+ )
+ pagination2 <- discussionsReads.commentReads.paginate(postID,
+ None,
+ Comment.Sorting.Hottest,
+ Paginated.Offset(10L),
+ Paginated.Limit(10)
+ )
} yield {
// then
pagination.entities must haveSize(10)
- pagination.nextOffset.map(_.value) must beSome(10L)
+ pagination.nextOffset.map(_.unwrap) must beSome(10L)
pagination2.entities must haveSize(10)
- pagination2.nextOffset.map(_.value) must beNone
+ pagination2.nextOffset.map(_.unwrap) must beNone
}
}
"paginate controversial Comments by Posts" in {
for {
// given
- channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.id)
+ channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.unwrap)
_ <- discussionsReads.channelReads.requireById(channelID).eventually()
- postID <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.id)
+ postID <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.unwrap)
_ <- discussionsReads.postReads.requireById(postID).eventually()
- post2ID <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.id)
+ post2ID <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.unwrap)
_ <- discussionsReads.postReads.requireById(post2ID).eventually()
paginatedData <- (0 until 20).toList.traverse(_ => commentCreate(postID))
- paginatedIDs <- paginatedData.traverse(discussionsWrites.commentWrites.createComment).map(_.map(_.id))
+ paginatedIDs <- paginatedData.traverse(discussionsWrites.commentWrites.createComment).map(_.map(_.unwrap))
nonPaginatedData <- (0 until 20).toList.traverse(_ => commentCreate(post2ID))
- nonPaginatedIds <- nonPaginatedData.traverse(discussionsWrites.commentWrites.createComment).map(_.map(_.id))
+ nonPaginatedIds <- nonPaginatedData.traverse(discussionsWrites.commentWrites.createComment).map(_.map(_.unwrap))
_ <- (paginatedIDs ++ nonPaginatedIds).traverse(discussionsReads.commentReads.requireById(_)).eventually()
user1ID <- voterIDCreate
user2ID <- voterIDCreate
@@ -357,18 +387,28 @@ final class CommentReadsWritesSpec extends Specification with DiscussionsIOTest
_ <- paginatedIDs
.traverse(discussionsReads.commentReads.requireById(_))
.assert("Votes should be eventually applied")(
- _.forall(e => e.data.totalScore.toInt > 0 || e.data.controversialScore.toNonNegativeInt.value > 0)
+ _.forall(e => e.data.totalScore.unwrap > 0 || e.data.controversialScore.unwrap > 0)
)
.eventually(delay = 1.second)
// when
- pagination <- discussionsReads.commentReads.paginate(postID, None, Comment.Sorting.Controversial, 0L, 10)
- pagination2 <- discussionsReads.commentReads.paginate(postID, None, Comment.Sorting.Controversial, 10L, 10)
+ pagination <- discussionsReads.commentReads.paginate(postID,
+ None,
+ Comment.Sorting.Controversial,
+ Paginated.Offset(0L),
+ Paginated.Limit(10)
+ )
+ pagination2 <- discussionsReads.commentReads.paginate(postID,
+ None,
+ Comment.Sorting.Controversial,
+ Paginated.Offset(10L),
+ Paginated.Limit(10)
+ )
} yield {
// then
pagination.entities must haveSize(10)
- pagination.nextOffset.map(_.value) must beSome(10L)
+ pagination.nextOffset.map(_.unwrap) must beSome(10L)
pagination2.entities must haveSize(10)
- pagination2.nextOffset.map(_.value) must beNone
+ pagination2.nextOffset.map(_.unwrap) must beNone
}
}
}
diff --git a/modules/discussions-impl/src/test/scala/io/branchtalk/discussions/DiscussionsFixtures.scala b/modules/discussions-impl/src/test/scala/io/branchtalk/discussions/DiscussionsFixtures.scala
new file mode 100644
index 00000000..603e544a
--- /dev/null
+++ b/modules/discussions-impl/src/test/scala/io/branchtalk/discussions/DiscussionsFixtures.scala
@@ -0,0 +1,39 @@
+package io.branchtalk.discussions
+
+import cats.effect.IO
+import io.branchtalk.discussions.model.*
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.Fixtures.*
+
+trait DiscussionsFixtures {
+
+ def editorIDCreate(using UUID.Generator): IO[ID[User]] = ID.create[IO, User]
+
+ def subscriberIDCreate(using UUID.Generator): IO[ID[User]] = ID.create[IO, User]
+
+ def voterIDCreate(using UUID.Generator): IO[ID[User]] = ID.create[IO, User]
+
+ def channelCreate(using UUID.Generator): IO[Channel.Create] =
+ (
+ ID.create[IO, User],
+ noWhitespaces.flatMap(ParseNewtype[IO].parse[Channel.UrlName](_)),
+ nameLike.flatMap(ParseNewtype[IO].parse[Channel.Name](_)),
+ textProducer.map(_.loremIpsum).flatMap(ParseNewtype[IO].parse[Channel.Description](_)).map(Option.apply)
+ ).mapN(Channel.Create.apply)
+
+ def postCreate(channelID: ID[Channel])(using UUID.Generator): IO[Post.Create] =
+ (
+ ID.create[IO, User],
+ channelID.pure[IO],
+ nameLike.flatMap(ParseNewtype[IO].parse[Post.Title](_)),
+ textProducer.map(_.loremIpsum).map(Post.Text(_)).map(Post.Content.Text(_))
+ ).mapN(Post.Create.apply)
+
+ def commentCreate(postID: ID[Post])(using UUID.Generator): IO[Comment.Create] =
+ (
+ ID.create[IO, User],
+ postID.pure[IO],
+ textProducer.map(_.loremIpsum).map(Comment.Content(_)),
+ none[ID[Comment]].pure[IO]
+ ).mapN(Comment.Create.apply)
+}
diff --git a/modules/discussions-impl/src/it/scala/io/branchtalk/discussions/DiscussionsIOTest.scala b/modules/discussions-impl/src/test/scala/io/branchtalk/discussions/DiscussionsIOTest.scala
similarity index 73%
rename from modules/discussions-impl/src/it/scala/io/branchtalk/discussions/DiscussionsIOTest.scala
rename to modules/discussions-impl/src/test/scala/io/branchtalk/discussions/DiscussionsIOTest.scala
index 979c47c7..f571a06c 100644
--- a/modules/discussions-impl/src/it/scala/io/branchtalk/discussions/DiscussionsIOTest.scala
+++ b/modules/discussions-impl/src/test/scala/io/branchtalk/discussions/DiscussionsIOTest.scala
@@ -1,16 +1,16 @@
package io.branchtalk.discussions
import cats.effect.{ IO, Resource }
-import io.branchtalk.shared.infrastructure.DomainConfig
+import io.branchtalk.shared.infrastructure.DomainModule
import io.branchtalk.{ IOTest, ResourcefulTest }
-import io.branchtalk.shared.model.UUIDGenerator
+import io.branchtalk.shared.model.*
-trait DiscussionsIOTest extends IOTest with ResourcefulTest {
+trait DiscussionsIOTest extends IOTest, ResourcefulTest {
- implicit protected def uuidGenerator: UUIDGenerator
+ protected given uuidGenerator: UUID.Generator
// populated by resources
- protected var discussionsCfg: DomainConfig = _
+ protected var discussionsCfg: DomainModule.Config = _
protected var discussionsReads: DiscussionsReads[IO] = _
protected var discussionsWrites: DiscussionsWrites[IO] = _
diff --git a/modules/discussions-impl/src/it/scala/io/branchtalk/discussions/PostReadsWritesSpec.scala b/modules/discussions-impl/src/test/scala/io/branchtalk/discussions/PostReadsWritesSpec.scala
similarity index 71%
rename from modules/discussions-impl/src/it/scala/io/branchtalk/discussions/PostReadsWritesSpec.scala
rename to modules/discussions-impl/src/test/scala/io/branchtalk/discussions/PostReadsWritesSpec.scala
index a6d2d661..8b8bacb7 100644
--- a/modules/discussions-impl/src/it/scala/io/branchtalk/discussions/PostReadsWritesSpec.scala
+++ b/modules/discussions-impl/src/test/scala/io/branchtalk/discussions/PostReadsWritesSpec.scala
@@ -3,14 +3,14 @@ package io.branchtalk.discussions
import cats.data.NonEmptySet
import cats.effect.IO
import io.branchtalk.discussions.model.{ Channel, Post }
-import io.branchtalk.shared.model.{ ID, TestUUIDGenerator, Updatable }
+import io.branchtalk.shared.model.*
import org.specs2.mutable.Specification
-import scala.concurrent.duration.DurationInt
+import scala.concurrent.duration.*
-final class PostReadsWritesSpec extends Specification with DiscussionsIOTest with DiscussionsFixtures {
+final class PostReadsWritesSpec extends Specification, DiscussionsIOTest, DiscussionsFixtures {
- implicit protected val uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
+ protected given uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
"Post Reads & Writes" should {
@@ -29,12 +29,12 @@ final class PostReadsWritesSpec extends Specification with DiscussionsIOTest wit
"create a Post and eventually read it" in {
for {
// given
- channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.id)
+ channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.unwrap)
_ <- discussionsReads.channelReads.requireById(channelID).eventually()
creationData <- (0 until 3).toList.traverse(_ => postCreate(channelID))
// when
toCreate <- creationData.traverse(discussionsWrites.postWrites.createPost)
- ids = toCreate.map(_.id)
+ ids = toCreate.map(_.unwrap)
posts <- ids.traverse(discussionsReads.postReads.requireById(_)).eventually()
postsOpt <- ids.traverse(discussionsReads.postReads.getById(_)).eventually()
postsExist <- ids.traverse(discussionsReads.postReads.exists).eventually()
@@ -51,7 +51,7 @@ final class PostReadsWritesSpec extends Specification with DiscussionsIOTest wit
"don't update a Post that doesn't exists" in {
for {
// given
- channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.id)
+ channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.unwrap)
_ <- discussionsReads.channelReads.requireById(channelID).eventually()
editorID <- editorIDCreate
creationData <- (0 until 3).toList.traverse(_ => postCreate(channelID))
@@ -75,12 +75,12 @@ final class PostReadsWritesSpec extends Specification with DiscussionsIOTest wit
"update an existing Post" in {
for {
// given
- channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.id)
+ channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.unwrap)
_ <- discussionsReads.channelReads.requireById(channelID).eventually()
editorID <- editorIDCreate
creationData <- (0 until 2).toList.traverse(_ => postCreate(channelID))
toCreate <- creationData.traverse(discussionsWrites.postWrites.createPost)
- ids = toCreate.map(_.id)
+ ids = toCreate.map(_.unwrap)
created <- ids.traverse(discussionsReads.postReads.requireById(_)).eventually()
updateData = created.zipWithIndex.collect {
case (Post(id, data), 0) =>
@@ -112,10 +112,10 @@ final class PostReadsWritesSpec extends Specification with DiscussionsIOTest wit
.collect {
case ((Post(_, older), Post(_, newer)), 0) =>
// set case
- older must_=== newer.copy(lastModifiedAt = None)
+ older === newer.copy(lastModifiedAt = None)
case ((Post(_, older), Post(_, newer)), 1) =>
// keep case
- older must_=== newer
+ older === newer
}
.lastOption
.getOrElse(true must beFalse)
@@ -124,11 +124,11 @@ final class PostReadsWritesSpec extends Specification with DiscussionsIOTest wit
"handle Upvoting and Downvoting of Posts" in {
for {
// given
- channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.id)
+ channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.unwrap)
_ <- discussionsReads.channelReads.requireById(channelID).eventually()
creationData <- (0 until 4).toList.traverse(_ => postCreate(channelID))
toCreate <- creationData.traverse(discussionsWrites.postWrites.createPost)
- ids = toCreate.map(_.id)
+ ids = toCreate.map(_.unwrap)
_ <- ids.traverse(discussionsReads.postReads.requireById(_)).eventually()
user0ID <- voterIDCreate
user1ID <- voterIDCreate
@@ -141,7 +141,7 @@ final class PostReadsWritesSpec extends Specification with DiscussionsIOTest wit
_ <- discussionsWrites.postWrites.downvotePost(Post.Downvote(ids(3), user3ID))
firstVotes <- ids
.traverse(discussionsReads.postReads.requireById(_))
- .assert("Posts should have first Votes applied")(_.forall(_.data.totalScore.toInt =!= 0))
+ .assert("Posts should have first Votes applied")(_.forall(_.data.totalScore.unwrap =!= 0))
.eventually(delay = 1.second)
_ <- discussionsWrites.postWrites.downvotePost(Post.Downvote(ids(0), user0ID))
_ <- discussionsWrites.postWrites.revokePostVote(Post.RevokeVote(ids(1), user1ID))
@@ -161,40 +161,40 @@ final class PostReadsWritesSpec extends Specification with DiscussionsIOTest wit
thirdsVotes <- ids
.traverse(discussionsReads.postReads.requireById(_))
.assert("Posts should have third Votes applied")(
- _.forall(p => p.data.totalScore.toInt > 0 || p.data.controversialScore.toNonNegativeInt.toInt > 0)
+ _.forall(p => p.data.totalScore.unwrap > 0 || p.data.controversialScore.unwrap > 0)
)
.eventually(delay = 1.second)
} yield {
// then
- firstVotes.map(_.data.totalScore) must_=== List(Post.TotalScore(1),
- Post.TotalScore(1),
- Post.TotalScore(-1),
- Post.TotalScore(-1)
+ firstVotes.map(_.data.totalScore) === List(Post.TotalScore(1),
+ Post.TotalScore(1),
+ Post.TotalScore(-1),
+ Post.TotalScore(-1)
)
- firstVotes.map(_.data.controversialScore) must_=== List(Post.ControversialScore(0),
- Post.ControversialScore(0),
- Post.ControversialScore(0),
- Post.ControversialScore(0)
+ firstVotes.map(_.data.controversialScore) === List(Post.ControversialScore(0),
+ Post.ControversialScore(0),
+ Post.ControversialScore(0),
+ Post.ControversialScore(0)
)
- secondsVotes.map(_.data.totalScore) must_=== List(Post.TotalScore(-1),
- Post.TotalScore(0),
- Post.TotalScore(1),
- Post.TotalScore(0)
+ secondsVotes.map(_.data.totalScore) === List(Post.TotalScore(-1),
+ Post.TotalScore(0),
+ Post.TotalScore(1),
+ Post.TotalScore(0)
)
- secondsVotes.map(_.data.controversialScore) must_=== List(Post.ControversialScore(0),
- Post.ControversialScore(0),
- Post.ControversialScore(0),
- Post.ControversialScore(0)
+ secondsVotes.map(_.data.controversialScore) === List(Post.ControversialScore(0),
+ Post.ControversialScore(0),
+ Post.ControversialScore(0),
+ Post.ControversialScore(0)
)
- thirdsVotes.map(_.data.totalScore) must_=== List(Post.TotalScore(0),
- Post.TotalScore(1),
- Post.TotalScore(2),
- Post.TotalScore(1)
+ thirdsVotes.map(_.data.totalScore) === List(Post.TotalScore(0),
+ Post.TotalScore(1),
+ Post.TotalScore(2),
+ Post.TotalScore(1)
)
- thirdsVotes.map(_.data.controversialScore) must_=== List(Post.ControversialScore(1),
- Post.ControversialScore(0),
- Post.ControversialScore(0),
- Post.ControversialScore(0)
+ thirdsVotes.map(_.data.controversialScore) === List(Post.ControversialScore(1),
+ Post.ControversialScore(0),
+ Post.ControversialScore(0),
+ Post.ControversialScore(0)
)
}
}
@@ -202,13 +202,13 @@ final class PostReadsWritesSpec extends Specification with DiscussionsIOTest wit
"allow delete and restore of a created Post" in {
for {
// given
- channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.id)
+ channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.unwrap)
_ <- discussionsReads.channelReads.requireById(channelID).eventually()
creationData <- (0 until 3).toList.traverse(_ => postCreate(channelID))
editorID <- editorIDCreate
// when
toCreate <- creationData.traverse(discussionsWrites.postWrites.createPost)
- ids = toCreate.map(_.id)
+ ids = toCreate.map(_.unwrap)
_ <- ids.traverse(discussionsReads.postReads.requireById(_)).eventually()
_ <- ids.map(Post.Delete(_, editorID)).traverse(discussionsWrites.postWrites.deletePost)
_ <- ids
@@ -243,39 +243,47 @@ final class PostReadsWritesSpec extends Specification with DiscussionsIOTest wit
"paginate newest Posts by Channels" in {
for {
// given
- channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.id)
+ channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.unwrap)
_ <- discussionsReads.channelReads.requireById(channelID).eventually()
- channel2ID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.id)
+ channel2ID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.unwrap)
_ <- discussionsReads.channelReads.requireById(channel2ID).eventually()
paginatedData <- (0 until 20).toList.traverse(_ => postCreate(channelID))
- paginatedIDs <- paginatedData.traverse(discussionsWrites.postWrites.createPost).map(_.map(_.id))
+ paginatedIDs <- paginatedData.traverse(discussionsWrites.postWrites.createPost).map(_.map(_.unwrap))
nonPaginatedData <- (0 until 20).toList.traverse(_ => postCreate(channel2ID))
- nonPaginatedIds <- nonPaginatedData.traverse(discussionsWrites.postWrites.createPost).map(_.map(_.id))
+ nonPaginatedIds <- nonPaginatedData.traverse(discussionsWrites.postWrites.createPost).map(_.map(_.unwrap))
_ <- (paginatedIDs ++ nonPaginatedIds).traverse(discussionsReads.postReads.requireById(_)).eventually()
channels = NonEmptySet.of(channelID)
// when
- pagination <- discussionsReads.postReads.paginate(channels, Post.Sorting.Newest, 0L, 10)
- pagination2 <- discussionsReads.postReads.paginate(channels, Post.Sorting.Newest, 10L, 10)
+ pagination <- discussionsReads.postReads.paginate(channels,
+ Post.Sorting.Newest,
+ Paginated.Offset(0L),
+ Paginated.Limit(10)
+ )
+ pagination2 <- discussionsReads.postReads.paginate(channels,
+ Post.Sorting.Newest,
+ Paginated.Offset(10L),
+ Paginated.Limit(10)
+ )
} yield {
// then
pagination.entities must haveSize(10)
- pagination.nextOffset.map(_.value) must beSome(10L)
+ pagination.nextOffset.map(_.unwrap) must beSome(10L)
pagination2.entities must haveSize(10)
- pagination2.nextOffset.map(_.value) must beNone
+ pagination2.nextOffset.map(_.unwrap) must beNone
}
}
"paginate hottest Posts by Channels" in {
for {
// given
- channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.id)
+ channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.unwrap)
_ <- discussionsReads.channelReads.requireById(channelID).eventually()
- channel2ID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.id)
+ channel2ID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.unwrap)
_ <- discussionsReads.channelReads.requireById(channel2ID).eventually()
paginatedData <- (0 until 20).toList.traverse(_ => postCreate(channelID))
- paginatedIDs <- paginatedData.traverse(discussionsWrites.postWrites.createPost).map(_.map(_.id))
+ paginatedIDs <- paginatedData.traverse(discussionsWrites.postWrites.createPost).map(_.map(_.unwrap))
nonPaginatedData <- (0 until 20).toList.traverse(_ => postCreate(channel2ID))
- nonPaginatedIds <- nonPaginatedData.traverse(discussionsWrites.postWrites.createPost).map(_.map(_.id))
+ nonPaginatedIds <- nonPaginatedData.traverse(discussionsWrites.postWrites.createPost).map(_.map(_.unwrap))
_ <- (paginatedIDs ++ nonPaginatedIds).traverse(discussionsReads.postReads.requireById(_)).eventually()
user1ID <- voterIDCreate
user2ID <- voterIDCreate
@@ -283,32 +291,40 @@ final class PostReadsWritesSpec extends Specification with DiscussionsIOTest wit
_ <- paginatedIDs.take(10).traverse(id => discussionsWrites.postWrites.upvotePost(Post.Upvote(id, user2ID)))
_ <- paginatedIDs
.traverse(discussionsReads.postReads.requireById(_))
- .assert("Votes should be eventually applied")(_.forall(_.data.totalScore.toInt > 0))
+ .assert("Votes should be eventually applied")(_.forall(_.data.totalScore.unwrap > 0))
.eventually(delay = 1.second)
channels = NonEmptySet.of(channelID)
// when
- pagination <- discussionsReads.postReads.paginate(channels, Post.Sorting.Hottest, 0L, 10)
- pagination2 <- discussionsReads.postReads.paginate(channels, Post.Sorting.Hottest, 10L, 10)
+ pagination <- discussionsReads.postReads.paginate(channels,
+ Post.Sorting.Hottest,
+ Paginated.Offset(0L),
+ Paginated.Limit(10)
+ )
+ pagination2 <- discussionsReads.postReads.paginate(channels,
+ Post.Sorting.Hottest,
+ Paginated.Offset(10L),
+ Paginated.Limit(10)
+ )
} yield {
// then
pagination.entities must haveSize(10)
- pagination.nextOffset.map(_.value) must beSome(10L)
+ pagination.nextOffset.map(_.unwrap) must beSome(10L)
pagination2.entities must haveSize(10)
- pagination2.nextOffset.map(_.value) must beNone
+ pagination2.nextOffset.map(_.unwrap) must beNone
}
}
"paginate controversial Posts by Channels" in {
for {
// given
- channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.id)
+ channelID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.unwrap)
_ <- discussionsReads.channelReads.requireById(channelID).eventually()
- channel2ID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.id)
+ channel2ID <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.unwrap)
_ <- discussionsReads.channelReads.requireById(channel2ID).eventually()
paginatedData <- (0 until 20).toList.traverse(_ => postCreate(channelID))
- paginatedIDs <- paginatedData.traverse(discussionsWrites.postWrites.createPost).map(_.map(_.id))
+ paginatedIDs <- paginatedData.traverse(discussionsWrites.postWrites.createPost).map(_.map(_.unwrap))
nonPaginatedData <- (0 until 20).toList.traverse(_ => postCreate(channel2ID))
- nonPaginatedIds <- nonPaginatedData.traverse(discussionsWrites.postWrites.createPost).map(_.map(_.id))
+ nonPaginatedIds <- nonPaginatedData.traverse(discussionsWrites.postWrites.createPost).map(_.map(_.unwrap))
_ <- (paginatedIDs ++ nonPaginatedIds).traverse(discussionsReads.postReads.requireById(_)).eventually()
user1ID <- voterIDCreate
user2ID <- voterIDCreate
@@ -317,19 +333,27 @@ final class PostReadsWritesSpec extends Specification with DiscussionsIOTest wit
_ <- paginatedIDs
.traverse(discussionsReads.postReads.requireById(_))
.assert("Votes should be eventually applied")(
- _.forall(e => e.data.totalScore.toInt > 0 || e.data.controversialScore.toNonNegativeInt.value > 0)
+ _.forall(e => e.data.totalScore.unwrap > 0 || e.data.controversialScore.unwrap > 0)
)
.eventually(delay = 1.second)
channels = NonEmptySet.of(channelID)
// when
- pagination <- discussionsReads.postReads.paginate(channels, Post.Sorting.Controversial, 0L, 10)
- pagination2 <- discussionsReads.postReads.paginate(channels, Post.Sorting.Controversial, 10L, 10)
+ pagination <- discussionsReads.postReads.paginate(channels,
+ Post.Sorting.Controversial,
+ Paginated.Offset(0L),
+ Paginated.Limit(10)
+ )
+ pagination2 <- discussionsReads.postReads.paginate(channels,
+ Post.Sorting.Controversial,
+ Paginated.Offset(10L),
+ Paginated.Limit(10)
+ )
} yield {
// then
pagination.entities must haveSize(10)
- pagination.nextOffset.map(_.value) must beSome(10L)
+ pagination.nextOffset.map(_.unwrap) must beSome(10L)
pagination2.entities must haveSize(10)
- pagination2.nextOffset.map(_.value) must beNone
+ pagination2.nextOffset.map(_.unwrap) must beNone
}
}
}
diff --git a/modules/discussions-impl/src/it/scala/io/branchtalk/discussions/SubscriptionReadsWritesSpec.scala b/modules/discussions-impl/src/test/scala/io/branchtalk/discussions/SubscriptionReadsWritesSpec.scala
similarity index 75%
rename from modules/discussions-impl/src/it/scala/io/branchtalk/discussions/SubscriptionReadsWritesSpec.scala
rename to modules/discussions-impl/src/test/scala/io/branchtalk/discussions/SubscriptionReadsWritesSpec.scala
index 79a83786..ecb6722a 100644
--- a/modules/discussions-impl/src/it/scala/io/branchtalk/discussions/SubscriptionReadsWritesSpec.scala
+++ b/modules/discussions-impl/src/test/scala/io/branchtalk/discussions/SubscriptionReadsWritesSpec.scala
@@ -1,12 +1,13 @@
package io.branchtalk.discussions
import io.branchtalk.discussions.model.Subscription
-import io.branchtalk.shared.model.TestUUIDGenerator
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.infrastructure.*
import org.specs2.mutable.Specification
-final class SubscriptionReadsWritesSpec extends Specification with DiscussionsIOTest with DiscussionsFixtures {
+final class SubscriptionReadsWritesSpec extends Specification, DiscussionsIOTest, DiscussionsFixtures {
- implicit protected val uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
+ protected given uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
"Subscription Reads & Writes" should {
@@ -15,18 +16,18 @@ final class SubscriptionReadsWritesSpec extends Specification with DiscussionsIO
// given
subscriberID <- subscriberIDCreate
ids <- (0 until 3).toList.traverse { _ =>
- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.id)
+ channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.unwrap)
}
_ <- ids.traverse(discussionsReads.channelReads.requireById(_)).eventually()
// when
_ <- discussionsWrites.subscriptionWrites.subscribe(Subscription.Subscribe(subscriberID, ids.toSet))
subscription <- discussionsReads.subscriptionReads
.requireForUser(subscriberID)
- .assert("Subscriptions should be eventually added")(_.subscriptions === ids.toSet)
+ .assert("Subscriptions should be eventually added")(_.subscriptions eqv ids.toSet)
.eventually()
} yield
// then
- subscription must_=== Subscription(subscriberID, ids.toSet)
+ subscription === Subscription(subscriberID, ids.toSet)
}
"remove Subscription and eventually read it" in {
@@ -34,17 +35,17 @@ final class SubscriptionReadsWritesSpec extends Specification with DiscussionsIO
// given
subscriberID <- subscriberIDCreate
idsToKeep <- (0 until 3).toList.traverse { _ =>
- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.id)
+ channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.unwrap)
}
idsToRemove <- (0 until 3).toList.traverse { _ =>
- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.id)
+ channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.unwrap)
}
- ids = (idsToKeep ++ idsToRemove)
+ ids = idsToKeep ++ idsToRemove
_ <- ids.traverse(discussionsReads.channelReads.requireById(_)).eventually()
_ <- discussionsWrites.subscriptionWrites.subscribe(Subscription.Subscribe(subscriberID, ids.toSet))
_ <- discussionsReads.subscriptionReads
.requireForUser(subscriberID)
- .assert("Subscriptions should be eventually added")(_.subscriptions === ids.toSet)
+ .assert("Subscriptions should be eventually added")(_.subscriptions eqv ids.toSet)
.eventually()
// when
Subscription.Scheduled(left) <- discussionsWrites.subscriptionWrites.unsubscribe(
@@ -52,12 +53,12 @@ final class SubscriptionReadsWritesSpec extends Specification with DiscussionsIO
)
subscription <- discussionsReads.subscriptionReads
.requireForUser(subscriberID)
- .assert("Subscriptions should be eventually deleted")(_.subscriptions === idsToKeep.toSet)
+ .assert("Subscriptions should be eventually deleted")(_.subscriptions eqv idsToKeep.toSet)
.eventually()
} yield {
// then
- left must_=== Subscription(subscriberID, idsToKeep.toSet)
- subscription must_=== Subscription(subscriberID, idsToKeep.toSet)
+ left === Subscription(subscriberID, idsToKeep.toSet)
+ subscription === Subscription(subscriberID, idsToKeep.toSet)
}
}
}
diff --git a/modules/discussions-impl/src/it/scala/io/branchtalk/discussions/TestDiscussionsConfig.scala b/modules/discussions-impl/src/test/scala/io/branchtalk/discussions/TestDiscussionsConfig.scala
similarity index 57%
rename from modules/discussions-impl/src/it/scala/io/branchtalk/discussions/TestDiscussionsConfig.scala
rename to modules/discussions-impl/src/test/scala/io/branchtalk/discussions/TestDiscussionsConfig.scala
index c77097c4..9f51b3b4 100644
--- a/modules/discussions-impl/src/it/scala/io/branchtalk/discussions/TestDiscussionsConfig.scala
+++ b/modules/discussions-impl/src/test/scala/io/branchtalk/discussions/TestDiscussionsConfig.scala
@@ -1,23 +1,15 @@
package io.branchtalk.discussions
import cats.effect.{ Async, Resource, Sync }
-import io.branchtalk.shared.infrastructure.{
- DomainConfig,
- DomainName,
- KafkaEventConsumerConfig,
- TestKafkaEventBusConfig,
- TestPostgresConfig,
- TestResources
-}
-import io.branchtalk.shared.infrastructure.PureconfigSupport._
-import io.scalaland.catnip.Semi
+import io.branchtalk.shared.infrastructure.*
+import io.branchtalk.shared.infrastructure.PureconfigSupport.{ *, given }
-@Semi(ConfigReader) final case class TestDiscussionsConfig(
+final case class TestDiscussionsConfig(
database: TestPostgresConfig,
publishedEventBus: TestKafkaEventBusConfig,
internalEventBus: TestKafkaEventBusConfig,
- consumers: Map[String, KafkaEventConsumerConfig]
-)
+ consumers: Map[String, KafkaEventBus.ConsumerConfig]
+) derives ConfigReader
object TestDiscussionsConfig {
def load[F[_]: Sync]: Resource[F, TestDiscussionsConfig] =
@@ -27,11 +19,17 @@ object TestDiscussionsConfig {
)
)
- def loadDomainConfig[F[_]: Async]: Resource[F, DomainConfig] =
+ def loadDomainConfig[F[_]: Async]: Resource[F, DomainModule.Config] =
for {
TestDiscussionsConfig(dbTest, publishedESTest, internalESTest, consumers) <- TestDiscussionsConfig.load[F]
db <- TestResources.postgresConfigResource[F](dbTest)
publishedES <- TestResources.kafkaEventBusConfigResource[F](publishedESTest)
internalES <- TestResources.kafkaEventBusConfigResource[F](internalESTest)
- } yield DomainConfig(DomainName("discussions-test"), db, db, publishedES, internalES, consumers)
+ } yield DomainModule.Config(DomainModule.Name.unsafeMake("discussions-test"),
+ db,
+ db,
+ publishedES,
+ internalES,
+ consumers
+ )
}
diff --git a/modules/discussions/src/main/scala/io/branchtalk/discussions/events/ChannelEvent.scala b/modules/discussions/src/main/scala/io/branchtalk/discussions/events/ChannelEvent.scala
index 1da865df..981b84d7 100644
--- a/modules/discussions/src/main/scala/io/branchtalk/discussions/events/ChannelEvent.scala
+++ b/modules/discussions/src/main/scala/io/branchtalk/discussions/events/ChannelEvent.scala
@@ -1,17 +1,15 @@
package io.branchtalk.discussions.events
-import com.sksamuel.avro4s._
-import io.branchtalk.ADT
+import com.sksamuel.avro4s.*
import io.branchtalk.discussions.model.{ Channel, User }
-import io.branchtalk.logging.CorrelationID
-import io.branchtalk.shared.model._
-import io.branchtalk.shared.model.AvroSupport._
-import io.scalaland.catnip.Semi
+import io.branchtalk.logging.*
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.model.AvroSupport.{ *, given }
-@Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) sealed trait ChannelEvent extends ADT
+sealed trait ChannelEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
object ChannelEvent {
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Created(
+ final case class Created(
id: ID[Channel],
authorID: ID[User],
urlName: Channel.UrlName,
@@ -19,9 +17,9 @@ object ChannelEvent {
description: Option[Channel.Description],
createdAt: CreationTime,
correlationID: CorrelationID
- ) extends ChannelEvent
+ ) extends ChannelEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Updated(
+ final case class Updated(
id: ID[Channel],
editorID: ID[User],
newUrlName: Updatable[Channel.UrlName],
@@ -29,17 +27,17 @@ object ChannelEvent {
newDescription: OptionUpdatable[Channel.Description],
modifiedAt: ModificationTime,
correlationID: CorrelationID
- ) extends ChannelEvent
+ ) extends ChannelEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Deleted(
+ final case class Deleted(
id: ID[Channel],
editorID: ID[User],
correlationID: CorrelationID
- ) extends ChannelEvent
+ ) extends ChannelEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Restored(
+ final case class Restored(
id: ID[Channel],
editorID: ID[User],
correlationID: CorrelationID
- ) extends ChannelEvent
+ ) extends ChannelEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
}
diff --git a/modules/discussions/src/main/scala/io/branchtalk/discussions/events/CommentEvent.scala b/modules/discussions/src/main/scala/io/branchtalk/discussions/events/CommentEvent.scala
index 4c7553df..7a4d53be 100644
--- a/modules/discussions/src/main/scala/io/branchtalk/discussions/events/CommentEvent.scala
+++ b/modules/discussions/src/main/scala/io/branchtalk/discussions/events/CommentEvent.scala
@@ -1,17 +1,15 @@
package io.branchtalk.discussions.events
-import com.sksamuel.avro4s._
-import io.branchtalk.ADT
+import com.sksamuel.avro4s.*
import io.branchtalk.discussions.model.{ Channel, Comment, Post, User }
-import io.branchtalk.logging.CorrelationID
-import io.branchtalk.shared.model._
-import io.branchtalk.shared.model.AvroSupport._
-import io.scalaland.catnip.Semi
+import io.branchtalk.logging.*
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.model.AvroSupport.{ *, given }
-@Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) sealed trait CommentEvent extends ADT
+sealed trait CommentEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
object CommentEvent {
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Created(
+ final case class Created(
id: ID[Comment],
authorID: ID[User],
channelID: ID[Channel],
@@ -20,43 +18,43 @@ object CommentEvent {
replyTo: Option[ID[Comment]],
createdAt: CreationTime,
correlationID: CorrelationID
- ) extends CommentEvent
+ ) extends CommentEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Updated(
+ final case class Updated(
id: ID[Comment],
editorID: ID[User],
newContent: Updatable[Comment.Content],
modifiedAt: ModificationTime,
correlationID: CorrelationID
- ) extends CommentEvent
+ ) extends CommentEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Deleted(
+ final case class Deleted(
id: ID[Comment],
editorID: ID[User],
correlationID: CorrelationID
- ) extends CommentEvent
+ ) extends CommentEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Restored(
+ final case class Restored(
id: ID[Comment],
editorID: ID[User],
correlationID: CorrelationID
- ) extends CommentEvent
+ ) extends CommentEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Upvoted(
+ final case class Upvoted(
id: ID[Comment],
voterID: ID[User],
correlationID: CorrelationID
- ) extends CommentEvent
+ ) extends CommentEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Downvoted(
+ final case class Downvoted(
id: ID[Comment],
voterID: ID[User],
correlationID: CorrelationID
- ) extends CommentEvent
+ ) extends CommentEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class VoteRevoked(
+ final case class VoteRevoked(
id: ID[Comment],
voterID: ID[User],
correlationID: CorrelationID
- ) extends CommentEvent
+ ) extends CommentEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
}
diff --git a/modules/discussions/src/main/scala/io/branchtalk/discussions/events/DiscussionEvent.scala b/modules/discussions/src/main/scala/io/branchtalk/discussions/events/DiscussionEvent.scala
index 3700e785..431f8bf6 100644
--- a/modules/discussions/src/main/scala/io/branchtalk/discussions/events/DiscussionEvent.scala
+++ b/modules/discussions/src/main/scala/io/branchtalk/discussions/events/DiscussionEvent.scala
@@ -1,23 +1,25 @@
package io.branchtalk.discussions.events
-import com.sksamuel.avro4s._
-import io.branchtalk.ADT
-import io.branchtalk.shared.model._
-import io.branchtalk.shared.model.AvroSupport._
-import io.scalaland.catnip.Semi
+import com.sksamuel.avro4s.*
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.model.AvroSupport.{ *, given }
-@Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) sealed trait DiscussionEvent extends ADT
+sealed trait DiscussionEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
object DiscussionEvent {
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor)
- final case class ForChannel(channel: ChannelEvent) extends DiscussionEvent
+ final case class ForChannel(
+ channel: ChannelEvent
+ ) extends DiscussionEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor)
- final case class ForComment(comment: CommentEvent) extends DiscussionEvent
+ final case class ForComment(
+ comment: CommentEvent
+ ) extends DiscussionEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(FastEq, ShowPretty, SchemaFor)
- final case class ForPost(post: PostEvent) extends DiscussionEvent
+ final case class ForPost(
+ post: PostEvent
+ ) extends DiscussionEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(FastEq, ShowPretty, SchemaFor)
- final case class ForSubscription(subscription: SubscriptionEvent) extends DiscussionEvent
+ final case class ForSubscription(
+ subscription: SubscriptionEvent
+ ) extends DiscussionEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
}
diff --git a/modules/discussions/src/main/scala/io/branchtalk/discussions/events/PostEvent.scala b/modules/discussions/src/main/scala/io/branchtalk/discussions/events/PostEvent.scala
index f56f2913..b0398671 100644
--- a/modules/discussions/src/main/scala/io/branchtalk/discussions/events/PostEvent.scala
+++ b/modules/discussions/src/main/scala/io/branchtalk/discussions/events/PostEvent.scala
@@ -1,17 +1,15 @@
package io.branchtalk.discussions.events
-import com.sksamuel.avro4s._
-import io.branchtalk.ADT
+import com.sksamuel.avro4s.*
import io.branchtalk.discussions.model.{ Channel, Post, User }
-import io.branchtalk.logging.CorrelationID
-import io.branchtalk.shared.model._
-import io.branchtalk.shared.model.AvroSupport._
-import io.scalaland.catnip.Semi
+import io.branchtalk.logging.*
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.model.AvroSupport.{ *, given }
-@Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) sealed trait PostEvent extends ADT
+sealed trait PostEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
object PostEvent {
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Created(
+ final case class Created(
id: ID[Post],
authorID: ID[User],
channelID: ID[Channel],
@@ -20,9 +18,9 @@ object PostEvent {
content: Post.Content,
createdAt: CreationTime,
correlationID: CorrelationID
- ) extends PostEvent
+ ) extends PostEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Updated(
+ final case class Updated(
id: ID[Post],
editorID: ID[User],
newUrlTitle: Updatable[Post.UrlTitle],
@@ -30,35 +28,35 @@ object PostEvent {
newContent: Updatable[Post.Content],
modifiedAt: ModificationTime,
correlationID: CorrelationID
- ) extends PostEvent
+ ) extends PostEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Deleted(
+ final case class Deleted(
id: ID[Post],
editorID: ID[User],
correlationID: CorrelationID
- ) extends PostEvent
+ ) extends PostEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Restored(
+ final case class Restored(
id: ID[Post],
editorID: ID[User],
correlationID: CorrelationID
- ) extends PostEvent
+ ) extends PostEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Upvoted(
+ final case class Upvoted(
id: ID[Post],
voterID: ID[User],
correlationID: CorrelationID
- ) extends PostEvent
+ ) extends PostEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Downvoted(
+ final case class Downvoted(
id: ID[Post],
voterID: ID[User],
correlationID: CorrelationID
- ) extends PostEvent
+ ) extends PostEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class VoteRevoked(
+ final case class VoteRevoked(
id: ID[Post],
voterID: ID[User],
correlationID: CorrelationID
- ) extends PostEvent
+ ) extends PostEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
}
diff --git a/modules/discussions/src/main/scala/io/branchtalk/discussions/events/SubscriptionEvent.scala b/modules/discussions/src/main/scala/io/branchtalk/discussions/events/SubscriptionEvent.scala
index 11b39958..2c84e2fd 100644
--- a/modules/discussions/src/main/scala/io/branchtalk/discussions/events/SubscriptionEvent.scala
+++ b/modules/discussions/src/main/scala/io/branchtalk/discussions/events/SubscriptionEvent.scala
@@ -1,27 +1,25 @@
package io.branchtalk.discussions.events
-import com.sksamuel.avro4s._
-import io.branchtalk.ADT
+import com.sksamuel.avro4s.*
import io.branchtalk.discussions.model.{ Channel, User }
import io.branchtalk.logging.CorrelationID
-import io.branchtalk.shared.model._
-import io.branchtalk.shared.model.AvroSupport._
-import io.scalaland.catnip.Semi
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.model.AvroSupport.{ *, given }
-@Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) sealed trait SubscriptionEvent extends ADT
+sealed trait SubscriptionEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
object SubscriptionEvent {
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Subscribed(
+ final case class Subscribed(
subscriberID: ID[User],
subscriptions: Set[ID[Channel]],
modifiedAt: ModificationTime,
correlationID: CorrelationID
- ) extends SubscriptionEvent
+ ) extends SubscriptionEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Unsubscribed(
+ final case class Unsubscribed(
subscriberID: ID[User],
subscriptions: Set[ID[Channel]],
modifiedAt: ModificationTime,
correlationID: CorrelationID
- ) extends SubscriptionEvent
+ ) extends SubscriptionEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
}
diff --git a/modules/discussions/src/main/scala/io/branchtalk/discussions/model/Channel.scala b/modules/discussions/src/main/scala/io/branchtalk/discussions/model/Channel.scala
index dd3a9d31..d9eefa0a 100644
--- a/modules/discussions/src/main/scala/io/branchtalk/discussions/model/Channel.scala
+++ b/modules/discussions/src/main/scala/io/branchtalk/discussions/model/Channel.scala
@@ -1,19 +1,90 @@
package io.branchtalk.discussions.model
-import io.scalaland.catnip.Semi
-import io.branchtalk.shared.model.{ CreationTime, FastEq, ID, ModificationTime, ShowPretty }
+import cats.{ Order, Show }
+import io.branchtalk.shared.model.*
-@Semi(FastEq, ShowPretty) final case class Channel(
+final case class Channel(
id: ID[Channel],
data: Channel.Data
-)
-object Channel extends ChannelProperties with ChannelCommands {
+) derives FastEq,
+ ShowPretty
+object Channel {
- @Semi(FastEq, ShowPretty) final case class Data(
+ final case class Data(
urlName: Channel.UrlName,
name: Channel.Name,
description: Option[Channel.Description],
createdAt: CreationTime,
lastModifiedAt: Option[ModificationTime]
- )
+ ) derives FastEq,
+ ShowPretty
+
+ final case class Create(
+ authorID: ID[User],
+ urlName: Channel.UrlName,
+ name: Channel.Name,
+ description: Option[Channel.Description]
+ ) derives FastEq,
+ ShowPretty
+
+ final case class Update(
+ id: ID[Channel],
+ editorID: ID[User],
+ newUrlName: Updatable[Channel.UrlName],
+ newName: Updatable[Channel.Name],
+ newDescription: OptionUpdatable[Channel.Description]
+ ) derives FastEq,
+ ShowPretty
+
+ final case class Delete(
+ id: ID[Channel],
+ editorID: ID[User]
+ ) derives FastEq,
+ ShowPretty
+
+ final case class Restore(
+ id: ID[Channel],
+ editorID: ID[User]
+ ) derives FastEq,
+ ShowPretty
+
+ type UrlName = UrlName.Type
+ object UrlName extends Newtype[String] {
+
+ private val pattern = "[A-Za-z0-9_-]+".r
+
+ override inline def validate(input: String): Boolean = pattern.matches(input)
+
+ def unapply(urlName: UrlName): Some[String] = Some(urlName.unwrap)
+
+ given Show[UrlName] = unsafeMakeF[Show](Show[String])
+ given Order[UrlName] = unsafeMakeF[Order](Order[String])
+ }
+
+ type Name = Name.Type
+ object Name extends Newtype[String] {
+
+ override inline def validate(input: String): Boolean = input.nonEmpty
+
+ def unapply(name: Name): Some[String] = Some(name.unwrap)
+
+ given Show[Name] = unsafeMakeF[Show](Show[String])
+ given Order[Name] = unsafeMakeF[Order](Order[String])
+ }
+
+ type Description = Description.Type
+ object Description extends Newtype[String] {
+
+ override inline def validate(input: String): Boolean = input.nonEmpty
+
+ def unapply(description: Description): Some[String] = Some(description.unwrap)
+
+ given Show[Description] = unsafeMakeF[Show](Show[String])
+ given Order[Description] = unsafeMakeF[Order](Order[String])
+ }
+
+ enum Sorting derives FastEq, ShowPretty {
+ case Newest
+ case Alphabetically
+ }
}
diff --git a/modules/discussions/src/main/scala/io/branchtalk/discussions/model/ChannelCommands.scala b/modules/discussions/src/main/scala/io/branchtalk/discussions/model/ChannelCommands.scala
deleted file mode 100644
index 84a3475d..00000000
--- a/modules/discussions/src/main/scala/io/branchtalk/discussions/model/ChannelCommands.scala
+++ /dev/null
@@ -1,42 +0,0 @@
-package io.branchtalk.discussions.model
-
-import io.scalaland.catnip.Semi
-import io.branchtalk.shared.model.{ FastEq, ID, OptionUpdatable, ShowPretty, Updatable }
-
-trait ChannelCommands { self: Channel.type =>
- type Create = ChannelCommands.Create
- type Update = ChannelCommands.Update
- type Delete = ChannelCommands.Delete
- type Restore = ChannelCommands.Restore
- val Create = ChannelCommands.Create
- val Update = ChannelCommands.Update
- val Delete = ChannelCommands.Delete
- val Restore = ChannelCommands.Restore
-}
-object ChannelCommands {
-
- @Semi(FastEq, ShowPretty) final case class Create(
- authorID: ID[User],
- urlName: Channel.UrlName,
- name: Channel.Name,
- description: Option[Channel.Description]
- )
-
- @Semi(FastEq, ShowPretty) final case class Update(
- id: ID[Channel],
- editorID: ID[User],
- newUrlName: Updatable[Channel.UrlName],
- newName: Updatable[Channel.Name],
- newDescription: OptionUpdatable[Channel.Description]
- )
-
- @Semi(FastEq, ShowPretty) final case class Delete(
- id: ID[Channel],
- editorID: ID[User]
- )
-
- @Semi(FastEq, ShowPretty) final case class Restore(
- id: ID[Channel],
- editorID: ID[User]
- )
-}
diff --git a/modules/discussions/src/main/scala/io/branchtalk/discussions/model/ChannelProperties.scala b/modules/discussions/src/main/scala/io/branchtalk/discussions/model/ChannelProperties.scala
deleted file mode 100644
index 31a4043d..00000000
--- a/modules/discussions/src/main/scala/io/branchtalk/discussions/model/ChannelProperties.scala
+++ /dev/null
@@ -1,64 +0,0 @@
-package io.branchtalk.discussions.model
-
-import cats.{ Order, Show }
-import cats.effect.Sync
-import enumeratum._
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.collection.NonEmpty
-import eu.timepit.refined.string.MatchesRegex
-import eu.timepit.refined.types.string.NonEmptyString
-import io.branchtalk.shared.model._
-import io.estatico.newtype.macros.newtype
-
-trait ChannelProperties { self: Channel.type =>
- type UrlName = ChannelProperties.UrlName
- type Name = ChannelProperties.Name
- type Description = ChannelProperties.Description
- type Sorting = ChannelProperties.Sorting
- val UrlName = ChannelProperties.UrlName
- val Name = ChannelProperties.Name
- val Description = ChannelProperties.Description
- val Sorting = ChannelProperties.Sorting
-}
-object ChannelProperties {
-
- @newtype final case class UrlName(urlString: String Refined MatchesRegex[UrlName.Pattern])
- object UrlName {
- type Pattern = "[A-Za-z0-9_-]+"
-
- def unapply(urlName: UrlName): Some[String Refined MatchesRegex[Pattern]] = Some(urlName.urlString)
- def parse[F[_]: Sync](string: String): F[UrlName] =
- ParseRefined[F].parse[MatchesRegex[Pattern]](string).map(UrlName.apply)
-
- implicit val show: Show[UrlName] = Show.wrap(_.urlString.value)
- implicit val order: Order[UrlName] = Order.by(_.urlString.value)
- }
-
- @newtype final case class Name(nonEmptyString: NonEmptyString)
- object Name {
- def unapply(name: Name): Some[NonEmptyString] = Some(name.nonEmptyString)
- def parse[F[_]: Sync](string: String): F[Name] =
- ParseRefined[F].parse[NonEmpty](string).map(Name.apply)
-
- implicit val show: Show[Name] = Show.wrap(_.nonEmptyString.value)
- implicit val order: Order[Name] = Order.by(_.nonEmptyString.value)
- }
-
- @newtype final case class Description(nonEmptyString: NonEmptyString)
- object Description {
- def unapply(description: Description): Some[NonEmptyString] = Some(description.nonEmptyString)
- def parse[F[_]: Sync](string: String): F[Description] =
- ParseRefined[F].parse[NonEmpty](string).map(Description.apply)
-
- implicit val show: Show[Description] = Show.wrap(_.nonEmptyString.value)
- implicit val eq: Order[Description] = Order.by(_.nonEmptyString.value)
- }
-
- sealed trait Sorting extends EnumEntry
- object Sorting extends Enum[Sorting] {
- case object Newest extends Sorting
- case object Alphabetically extends Sorting
-
- val values = findValues
- }
-}
diff --git a/modules/discussions/src/main/scala/io/branchtalk/discussions/model/Comment.scala b/modules/discussions/src/main/scala/io/branchtalk/discussions/model/Comment.scala
index 2941d594..2be87756 100644
--- a/modules/discussions/src/main/scala/io/branchtalk/discussions/model/Comment.scala
+++ b/modules/discussions/src/main/scala/io/branchtalk/discussions/model/Comment.scala
@@ -1,15 +1,21 @@
package io.branchtalk.discussions.model
-import io.scalaland.catnip.Semi
-import io.branchtalk.shared.model._
+import java.net.URI
-@Semi(FastEq, ShowPretty) final case class Comment(
+import cats.effect.Sync
+import cats.{ Order, Show }
+import enumeratum.*
+import enumeratum.EnumEntry.Hyphencase
+import io.branchtalk.shared.model.*
+
+final case class Comment(
id: ID[Comment],
data: Comment.Data
-)
-object Comment extends CommentProperties with CommentCommands {
+) derives FastEq,
+ ShowPretty
+object Comment {
- @Semi(FastEq, ShowPretty) final case class Data(
+ final case class Data(
authorID: ID[User],
channelID: ID[Channel],
postID: ID[Post],
@@ -23,5 +29,128 @@ object Comment extends CommentProperties with CommentCommands {
downvores: Comment.Downvotes,
totalScore: Comment.TotalScore,
controversialScore: Comment.ControversialScore
- )
+ ) derives FastEq,
+ ShowPretty
+
+ final case class Create(
+ authorID: ID[User],
+ postID: ID[Post],
+ content: Comment.Content,
+ replyTo: Option[ID[Comment]]
+ ) derives FastEq,
+ ShowPretty
+
+ final case class Update(
+ id: ID[Comment],
+ editorID: ID[User],
+ newContent: Updatable[Comment.Content]
+ ) derives FastEq,
+ ShowPretty
+
+ final case class Delete(
+ id: ID[Comment],
+ editorID: ID[User]
+ ) derives FastEq,
+ ShowPretty
+
+ final case class Restore(
+ id: ID[Comment],
+ editorID: ID[User]
+ ) derives FastEq,
+ ShowPretty
+
+ final case class Upvote(
+ id: ID[Comment],
+ voterID: ID[User]
+ ) derives FastEq,
+ ShowPretty
+
+ final case class Downvote(
+ id: ID[Comment],
+ voterID: ID[User]
+ ) derives FastEq,
+ ShowPretty
+
+ final case class RevokeVote(
+ id: ID[Comment],
+ voterID: ID[User]
+ ) derives FastEq,
+ ShowPretty
+
+ type Content = Content.Type
+ object Content extends Newtype[String] {
+ def unapply(content: Content): Some[String] = Some(content.unwrap)
+
+ given Show[Content] = unsafeMakeF[Show](Show[String])
+ given Order[Content] = unsafeMakeF[Order](Order[String])
+ }
+
+ type NestingLevel = NestingLevel.Type
+ object NestingLevel extends Newtype[Int] {
+
+ override inline def validate(input: Int): Boolean = input >= 0
+
+ def unapply(nestingLevel: NestingLevel): Some[Int] = Some(nestingLevel.unwrap)
+
+ given Show[NestingLevel] = unsafeMakeF[Show](Show[Int])
+ given Order[NestingLevel] = unsafeMakeF[Order](Order[Int])
+ }
+
+ type RepliesNr = RepliesNr.Type
+ object RepliesNr extends Newtype[Int] {
+
+ override inline def validate(input: Int): Boolean = input >= 0
+
+ def unapply(repliesNr: RepliesNr): Some[Int] = Some(repliesNr.unwrap)
+
+ given Show[RepliesNr] = unsafeMakeF[Show](Show[Int])
+ given Order[RepliesNr] = unsafeMakeF[Order](Order[Int])
+ }
+
+ type Upvotes = Upvotes.Type
+ object Upvotes extends Newtype[Int] {
+
+ override inline def validate(input: Int): Boolean = input >= 0
+
+ def unapply(upvotes: Upvotes): Some[Int] = Some(upvotes.unwrap)
+
+ given Show[Upvotes] = unsafeMakeF[Show](Show[Int])
+ given Order[Upvotes] = unsafeMakeF[Order](Order[Int])
+ }
+
+ type Downvotes = Downvotes.Type
+ object Downvotes extends Newtype[Int] {
+
+ override inline def validate(input: Int): Boolean = input >= 0
+
+ def unapply(downvotes: Downvotes): Some[Int] = Some(downvotes.unwrap)
+
+ given Show[Downvotes] = unsafeMakeF[Show](Show[Int])
+ given Order[Downvotes] = unsafeMakeF[Order](Order[Int])
+ }
+
+ type TotalScore = TotalScore.Type
+ object TotalScore extends Newtype[Int] {
+ def unapply(content: TotalScore): Some[Int] = Some(content.unwrap)
+
+ given Show[TotalScore] = unsafeMakeF[Show](Show[Int])
+ given Order[TotalScore] = unsafeMakeF[Order](Order[Int])
+ }
+
+ type ControversialScore = ControversialScore.Type
+ object ControversialScore extends Newtype[Int] {
+
+ override inline def validate(input: Int): Boolean = input >= 0
+
+ def unapply(controversialScore: ControversialScore): Some[Int] = Some(controversialScore.unwrap)
+
+ given Show[ControversialScore] = unsafeMakeF[Show](Show[Int])
+ given Order[ControversialScore] = unsafeMakeF[Order](Order[Int])
+ }
+
+ enum Sorting derives FastEq, ShowPretty {
+ case Newest
+ case Hottest
+ case Controversial
+ }
}
diff --git a/modules/discussions/src/main/scala/io/branchtalk/discussions/model/CommentCommands.scala b/modules/discussions/src/main/scala/io/branchtalk/discussions/model/CommentCommands.scala
deleted file mode 100644
index eb0a1b09..00000000
--- a/modules/discussions/src/main/scala/io/branchtalk/discussions/model/CommentCommands.scala
+++ /dev/null
@@ -1,61 +0,0 @@
-package io.branchtalk.discussions.model
-
-import io.scalaland.catnip.Semi
-import io.branchtalk.shared.model.{ FastEq, ID, ShowPretty, Updatable }
-
-trait CommentCommands { self: Comment.type =>
- type Create = CommentCommands.Create
- type Update = CommentCommands.Update
- type Delete = CommentCommands.Delete
- type Restore = CommentCommands.Restore
- type Upvote = CommentCommands.Upvote
- type Downvote = CommentCommands.Downvote
- type RevokeVote = CommentCommands.RevokeVote
- val Create = CommentCommands.Create
- val Update = CommentCommands.Update
- val Delete = CommentCommands.Delete
- val Restore = CommentCommands.Restore
- val Upvote = CommentCommands.Upvote
- val Downvote = CommentCommands.Downvote
- val RevokeVote = CommentCommands.RevokeVote
-}
-object CommentCommands {
-
- @Semi(FastEq, ShowPretty) final case class Create(
- authorID: ID[User],
- postID: ID[Post],
- content: Comment.Content,
- replyTo: Option[ID[Comment]]
- )
-
- @Semi(FastEq, ShowPretty) final case class Update(
- id: ID[Comment],
- editorID: ID[User],
- newContent: Updatable[Comment.Content]
- )
-
- @Semi(FastEq, ShowPretty) final case class Delete(
- id: ID[Comment],
- editorID: ID[User]
- )
-
- @Semi(FastEq, ShowPretty) final case class Restore(
- id: ID[Comment],
- editorID: ID[User]
- )
-
- @Semi(FastEq, ShowPretty) final case class Upvote(
- id: ID[Comment],
- voterID: ID[User]
- )
-
- @Semi(FastEq, ShowPretty) final case class Downvote(
- id: ID[Comment],
- voterID: ID[User]
- )
-
- @Semi(FastEq, ShowPretty) final case class RevokeVote(
- id: ID[Comment],
- voterID: ID[User]
- )
-}
diff --git a/modules/discussions/src/main/scala/io/branchtalk/discussions/model/CommentProperties.scala b/modules/discussions/src/main/scala/io/branchtalk/discussions/model/CommentProperties.scala
deleted file mode 100644
index db634abe..00000000
--- a/modules/discussions/src/main/scala/io/branchtalk/discussions/model/CommentProperties.scala
+++ /dev/null
@@ -1,99 +0,0 @@
-package io.branchtalk.discussions.model
-
-import cats.{ Order, Show }
-import cats.effect.Sync
-import enumeratum._
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.numeric.NonNegative
-import io.branchtalk.shared.model._
-import io.estatico.newtype.macros.newtype
-import io.estatico.newtype.ops._
-
-trait CommentProperties { self: Comment.type =>
- type Content = CommentProperties.Content
- type NestingLevel = CommentProperties.NestingLevel
- type RepliesNr = CommentProperties.RepliesNr
- type Upvotes = CommentProperties.Upvotes
- type Downvotes = CommentProperties.Downvotes
- type TotalScore = CommentProperties.TotalScore
- type ControversialScore = CommentProperties.ControversialScore
- type Sorting = CommentProperties.Sorting
- val Content = CommentProperties.Content
- val NestingLevel = CommentProperties.NestingLevel
- val RepliesNr = CommentProperties.RepliesNr
- val Upvotes = CommentProperties.Upvotes
- val Downvotes = CommentProperties.Downvotes
- val TotalScore = CommentProperties.TotalScore
- val ControversialScore = CommentProperties.ControversialScore
- val Sorting = CommentProperties.Sorting
-}
-object CommentProperties {
-
- @newtype final case class Content(string: String)
- object Content {
- def unapply(content: Content): Some[String] = Some(content.string)
-
- implicit val show: Show[Content] = Show.wrap(_.string)
- implicit val order: Order[Content] = Order[String].coerce
- }
-
- @newtype final case class NestingLevel(nonNegativeInt: Int Refined NonNegative)
- object NestingLevel {
- def unapply(nestingLevel: NestingLevel): Some[Int Refined NonNegative] = Some(nestingLevel.nonNegativeInt)
- def parse[F[_]: Sync](int: Int): F[NestingLevel] =
- ParseRefined[F].parse[NonNegative](int).map(NestingLevel.apply)
-
- implicit val show: Show[NestingLevel] = Show.wrap(_.nonNegativeInt.value)
- implicit val order: Order[NestingLevel] = Order.by(_.nonNegativeInt.value)
- }
-
- @newtype final case class RepliesNr(toNonNegativeInt: Int Refined NonNegative)
- object RepliesNr {
- def unapply(repliesNr: RepliesNr): Some[Int Refined NonNegative] = Some(repliesNr.toNonNegativeInt)
-
- implicit val show: Show[RepliesNr] = Show.wrap(_.toNonNegativeInt.value)
- implicit val order: Order[RepliesNr] = Order.by(_.toNonNegativeInt.value)
- }
-
- @newtype final case class Upvotes(toNonNegativeInt: Int Refined NonNegative)
- object Upvotes {
- def unapply(upvotes: Upvotes): Some[Int Refined NonNegative] = Some(upvotes.toNonNegativeInt)
-
- implicit val show: Show[Upvotes] = Show.wrap(_.toNonNegativeInt.value)
- implicit val order: Order[Upvotes] = Order.by(_.toNonNegativeInt.value)
- }
-
- @newtype final case class Downvotes(toNonNegativeInt: Int Refined NonNegative)
- object Downvotes {
- def unapply(downvotes: Downvotes): Some[Int Refined NonNegative] = Some(downvotes.toNonNegativeInt)
-
- implicit val show: Show[Downvotes] = Show.wrap(_.toNonNegativeInt.value)
- implicit val order: Order[Downvotes] = Order.by(_.toNonNegativeInt.value)
- }
-
- @newtype final case class TotalScore(toInt: Int)
- object TotalScore {
- def unapply(totalScore: TotalScore): Some[Int] = Some(totalScore.toInt)
-
- implicit val show: Show[TotalScore] = Show.wrap(_.toInt)
- implicit val order: Order[TotalScore] = Order[Int].coerce
- }
-
- @newtype final case class ControversialScore(toNonNegativeInt: Int Refined NonNegative)
- object ControversialScore {
- def unapply(controversialScore: ControversialScore): Some[Int Refined NonNegative] =
- Some(controversialScore.toNonNegativeInt)
-
- implicit val show: Show[ControversialScore] = Show.wrap(_.toNonNegativeInt.value)
- implicit val order: Order[ControversialScore] = Order.by(_.toNonNegativeInt.value)
- }
-
- sealed trait Sorting extends EnumEntry
- object Sorting extends Enum[Sorting] {
- case object Newest extends Sorting
- case object Hottest extends Sorting
- case object Controversial extends Sorting
-
- val values = findValues
- }
-}
diff --git a/modules/discussions/src/main/scala/io/branchtalk/discussions/model/Post.scala b/modules/discussions/src/main/scala/io/branchtalk/discussions/model/Post.scala
index 33c97a52..186c2b4d 100644
--- a/modules/discussions/src/main/scala/io/branchtalk/discussions/model/Post.scala
+++ b/modules/discussions/src/main/scala/io/branchtalk/discussions/model/Post.scala
@@ -1,15 +1,21 @@
package io.branchtalk.discussions.model
-import io.scalaland.catnip.Semi
-import io.branchtalk.shared.model._
+import java.net.URI
-@Semi(FastEq, ShowPretty) final case class Post(
+import cats.effect.Sync
+import cats.{ Order, Show }
+import enumeratum.*
+import enumeratum.EnumEntry.Hyphencase
+import io.branchtalk.shared.model.*
+
+final case class Post(
id: ID[Post],
data: Post.Data
-)
-object Post extends PostProperties with PostCommands {
+) derives FastEq,
+ ShowPretty
+object Post {
- @Semi(FastEq, ShowPretty) final case class Data(
+ final case class Data(
authorID: ID[User],
channelID: ID[Channel],
urlTitle: Post.UrlTitle,
@@ -22,5 +28,184 @@ object Post extends PostProperties with PostCommands {
downvotes: Post.Downvotes,
totalScore: Post.TotalScore,
controversialScore: Post.ControversialScore
- )
+ ) derives FastEq,
+ ShowPretty
+
+ final case class Create(
+ authorID: ID[User],
+ channelID: ID[Channel],
+ title: Post.Title,
+ content: Post.Content
+ ) derives FastEq,
+ ShowPretty
+
+ final case class Update(
+ id: ID[Post],
+ editorID: ID[User],
+ newTitle: Updatable[Post.Title],
+ newContent: Updatable[Post.Content]
+ ) derives FastEq,
+ ShowPretty
+
+ final case class Delete(
+ id: ID[Post],
+ editorID: ID[User]
+ ) derives FastEq,
+ ShowPretty
+
+ final case class Restore(
+ id: ID[Post],
+ editorID: ID[User]
+ ) derives FastEq,
+ ShowPretty
+
+ final case class Upvote(
+ id: ID[Post],
+ voterID: ID[User]
+ ) derives FastEq,
+ ShowPretty
+
+ final case class Downvote(
+ id: ID[Post],
+ voterID: ID[User]
+ ) derives FastEq,
+ ShowPretty
+
+ final case class RevokeVote(
+ id: ID[Post],
+ voterID: ID[User]
+ ) derives FastEq,
+ ShowPretty
+
+ type UrlTitle = UrlTitle.Type
+ object UrlTitle extends Newtype[String] {
+
+ override inline def validate(input: String): Boolean = input.nonEmpty
+
+ def unapply(name: UrlTitle): Some[String] = Some(name.unwrap)
+
+ given Show[UrlTitle] = unsafeMakeF[Show](Show[String])
+ given Order[UrlTitle] = unsafeMakeF[Order](Order[String])
+ }
+
+ type Title = Title.Type
+ object Title extends Newtype[String] {
+
+ override inline def validate(input: String): Boolean = input.nonEmpty
+
+ def unapply(name: Title): Some[String] = Some(name.unwrap)
+
+ given Show[Title] = unsafeMakeF[Show](Show[String])
+ given Order[Title] = unsafeMakeF[Order](Order[String])
+ }
+
+ type URL = URL.Type
+ object URL extends Newtype[URI] {
+ def unapply(name: URL): Some[URI] = Some(name.unwrap)
+
+ @SuppressWarnings(Array("org.wartremover.warts.ToString")) // false warning - URI overrides toString
+ given Show[URL] = _.unwrap.toString
+ given Order[URL] = unsafeMakeF[Order](Order.fromComparable[URI])
+ }
+
+ type Text = Text.Type
+ object Text extends Newtype[String] {
+ def unapply(name: Text): Some[String] = Some(name.unwrap)
+
+ given Show[Text] = unsafeMakeF[Show](Show[String]) // without wrapper because it lives only within Context.Text
+ given Order[Text] = unsafeMakeF[Order](Order[String])
+ }
+
+ enum Content derives FastEq, ShowPretty {
+ case Url(url: Post.URL)
+ case Text(text: Post.Text)
+ }
+ object Content {
+
+ enum Type extends EnumEntry, Hyphencase derives FastEq, ShowPretty {
+ case Url
+ case Text
+ }
+
+ type Raw = Raw.Type
+ object Raw extends Newtype[String] {
+ def unapply(name: Raw): Some[String] = Some(name.unwrap)
+
+ given Show[Raw] = unsafeMakeF[Show](Show[String])
+ given Order[Raw] = unsafeMakeF[Order](Order[String])
+ }
+
+ object Tupled {
+ def apply(contentType: Type, contentText: Raw): Content = contentType match {
+ case Type.Url => Content.Url(Post.URL.unsafeMake(URI.create(contentText.unwrap)))
+ case Type.Text => Content.Text(Post.Text.unsafeMake(contentText.unwrap))
+ }
+
+ @SuppressWarnings(Array("org.wartremover.warts.ToString")) // false warning - URI overrides toString
+ def unpack(content: Content): (Type, Raw) = content match {
+ case Content.Url(url) => Type.Url -> Raw.unsafeMake(url.unwrap.toString)
+ case Content.Text(text) => Type.Text -> Raw.unsafeMake(text.unwrap)
+ }
+
+ def unapply(content: Content): Some[(Type, Raw)] = Some(unpack(content))
+ }
+ }
+
+ type CommentsNr = CommentsNr.Type
+ object CommentsNr extends Newtype[Int] {
+
+ override inline def validate(input: Int): Boolean = input >= 0
+
+ def unapply(downvotes: CommentsNr): Some[Int] = Some(downvotes.unwrap)
+
+ given Show[CommentsNr] = unsafeMakeF[Show](Show[Int])
+ given Order[CommentsNr] = unsafeMakeF[Order](Order[Int])
+ }
+
+ type Upvotes = Upvotes.Type
+ object Upvotes extends Newtype[Int] {
+
+ override inline def validate(input: Int): Boolean = input >= 0
+
+ def unapply(downvotes: Upvotes): Some[Int] = Some(downvotes.unwrap)
+
+ given Show[Upvotes] = unsafeMakeF[Show](Show[Int])
+ given Order[Upvotes] = unsafeMakeF[Order](Order[Int])
+ }
+
+ type Downvotes = Downvotes.Type
+ object Downvotes extends Newtype[Int] {
+
+ override inline def validate(input: Int): Boolean = input >= 0
+
+ def unapply(downvotes: Downvotes): Some[Int] = Some(downvotes.unwrap)
+
+ given Show[Downvotes] = unsafeMakeF[Show](Show[Int])
+ given Order[Downvotes] = unsafeMakeF[Order](Order[Int])
+ }
+
+ type TotalScore = TotalScore.Type
+ object TotalScore extends Newtype[Int] {
+ def unapply(content: TotalScore): Some[Int] = Some(content.unwrap)
+
+ given Show[TotalScore] = unsafeMakeF[Show](Show[Int])
+ given Order[TotalScore] = unsafeMakeF[Order](Order[Int])
+ }
+
+ type ControversialScore = ControversialScore.Type
+ object ControversialScore extends Newtype[Int] {
+
+ override inline def validate(input: Int): Boolean = input >= 0
+
+ def unapply(controversialScore: ControversialScore): Some[Int] = Some(controversialScore.unwrap)
+
+ given Show[ControversialScore] = unsafeMakeF[Show](Show[Int])
+ given Order[ControversialScore] = unsafeMakeF[Order](Order[Int])
+ }
+
+ enum Sorting derives FastEq, ShowPretty {
+ case Newest
+ case Hottest
+ case Controversial
+ }
}
diff --git a/modules/discussions/src/main/scala/io/branchtalk/discussions/model/PostCommands.scala b/modules/discussions/src/main/scala/io/branchtalk/discussions/model/PostCommands.scala
deleted file mode 100644
index 0710434c..00000000
--- a/modules/discussions/src/main/scala/io/branchtalk/discussions/model/PostCommands.scala
+++ /dev/null
@@ -1,62 +0,0 @@
-package io.branchtalk.discussions.model
-
-import io.scalaland.catnip.Semi
-import io.branchtalk.shared.model.{ FastEq, ID, ShowPretty, Updatable }
-
-trait PostCommands { self: Post.type =>
- type Create = PostCommands.Create
- type Update = PostCommands.Update
- type Delete = PostCommands.Delete
- type Restore = PostCommands.Restore
- type Upvote = PostCommands.Upvote
- type Downvote = PostCommands.Downvote
- type RevokeVote = PostCommands.RevokeVote
- val Create = PostCommands.Create
- val Update = PostCommands.Update
- val Delete = PostCommands.Delete
- val Restore = PostCommands.Restore
- val Upvote = PostCommands.Upvote
- val Downvote = PostCommands.Downvote
- val RevokeVote = PostCommands.RevokeVote
-}
-object PostCommands {
-
- @Semi(FastEq, ShowPretty) final case class Create(
- authorID: ID[User],
- channelID: ID[Channel],
- title: Post.Title,
- content: Post.Content
- )
-
- @Semi(FastEq, ShowPretty) final case class Update(
- id: ID[Post],
- editorID: ID[User],
- newTitle: Updatable[Post.Title],
- newContent: Updatable[Post.Content]
- )
-
- @Semi(FastEq, ShowPretty) final case class Delete(
- id: ID[Post],
- editorID: ID[User]
- )
-
- @Semi(FastEq, ShowPretty) final case class Restore(
- id: ID[Post],
- editorID: ID[User]
- )
-
- @Semi(FastEq, ShowPretty) final case class Upvote(
- id: ID[Post],
- voterID: ID[User]
- )
-
- @Semi(FastEq, ShowPretty) final case class Downvote(
- id: ID[Post],
- voterID: ID[User]
- )
-
- @Semi(FastEq, ShowPretty) final case class RevokeVote(
- id: ID[Post],
- voterID: ID[User]
- )
-}
diff --git a/modules/discussions/src/main/scala/io/branchtalk/discussions/model/PostProperties.scala b/modules/discussions/src/main/scala/io/branchtalk/discussions/model/PostProperties.scala
deleted file mode 100644
index 866e3fb4..00000000
--- a/modules/discussions/src/main/scala/io/branchtalk/discussions/model/PostProperties.scala
+++ /dev/null
@@ -1,164 +0,0 @@
-package io.branchtalk.discussions.model
-
-import java.net.URI
-
-import cats.effect.Sync
-import cats.{ Order, Show }
-import enumeratum._
-import enumeratum.EnumEntry.Hyphencase
-import io.branchtalk.ADT
-import io.branchtalk.shared.model._
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.collection.NonEmpty
-import eu.timepit.refined.numeric.NonNegative
-import eu.timepit.refined.types.string.NonEmptyString
-import io.estatico.newtype.macros.newtype
-import io.scalaland.catnip.Semi
-
-trait PostProperties { self: Post.type =>
- type UrlTitle = PostProperties.UrlTitle
- type Title = PostProperties.Title
- type URL = PostProperties.URL
- type Text = PostProperties.Text
- type Content = PostProperties.Content
- type CommentsNr = PostProperties.CommentsNr
- type Upvotes = PostProperties.Upvotes
- type Downvotes = PostProperties.Downvotes
- type TotalScore = PostProperties.TotalScore
- type ControversialScore = PostProperties.ControversialScore
- type Sorting = PostProperties.Sorting
- val UrlTitle = PostProperties.UrlTitle
- val Title = PostProperties.Title
- val URL = PostProperties.URL
- val Text = PostProperties.Text
- val Content = PostProperties.Content
- val CommentsNr = PostProperties.CommentsNr
- val Upvotes = PostProperties.Upvotes
- val Downvotes = PostProperties.Downvotes
- val TotalScore = PostProperties.TotalScore
- val ControversialScore = PostProperties.ControversialScore
- val Sorting = PostProperties.Sorting
-}
-object PostProperties {
-
- @newtype final case class UrlTitle(nonEmptyString: NonEmptyString)
- object UrlTitle {
- def unapply(urlTitle: UrlTitle): Some[NonEmptyString] = Some(urlTitle.nonEmptyString)
- def parse[F[_]: Sync](string: String): F[UrlTitle] =
- ParseRefined[F].parse[NonEmpty](string).map(UrlTitle.apply)
-
- implicit val show: Show[UrlTitle] = Show.wrap(_.nonEmptyString.value)
- implicit val order: Order[UrlTitle] = Order.by(_.nonEmptyString.value)
- }
-
- @newtype final case class Title(nonEmptyString: NonEmptyString)
- object Title {
- def unapply(title: Title): Some[NonEmptyString] = Some(title.nonEmptyString)
- def parse[F[_]: Sync](string: String): F[Title] =
- ParseRefined[F].parse[NonEmpty](string).map(Title.apply)
-
- implicit val show: Show[Title] = Show.wrap(_.nonEmptyString.value)
- implicit val order: Order[Title] = Order.by(_.nonEmptyString.value)
- }
-
- @newtype final case class URL(uri: URI)
- object URL {
- def unapply(url: URL): Some[URI] = Some(url.uri)
-
- implicit val show: Show[URL] = _.uri.toString // without wrapper because it lives only within Context.Url
- implicit val order: Order[URL] = Order.by[URL, URI](_.uri)(Order.fromComparable)
- }
-
- @newtype final case class Text(string: String)
- object Text {
- def unapply(text: Text): Some[String] = Some(text.string)
-
- implicit val show: Show[Text] = _.string // without wrapper because it lives only within Context.Text
- implicit val order: Order[Text] = Order.by(_.string)
- }
-
- @Semi(FastEq, ShowPretty) sealed trait Content extends ADT
- object Content {
- final case class Url(url: Post.URL) extends Content
- final case class Text(text: Post.Text) extends Content
-
- @Semi(FastEq, ShowPretty) sealed trait Type extends EnumEntry with Hyphencase
- object Type extends Enum[Type] {
- case object Url extends Type
- case object Text extends Type
-
- val values: IndexedSeq[Type] = findValues
- }
- @newtype final case class Raw(string: String)
- object Raw {
- def unapply(raw: Raw): Some[String] = Some(raw.string)
-
- implicit val show: Show[Raw] = Show.wrap(_.string)
- implicit val order: Order[Raw] = Order.by(_.string)
- }
-
- object Tupled {
- def apply(contentType: Type, contentText: Raw): Content = contentType match {
- case Type.Url => Content.Url(Post.URL(URI.create(contentText.string)))
- case Type.Text => Content.Text(Post.Text(contentText.string))
- }
-
- def unpack(content: Content): (Type, Raw) = content match {
- case Content.Url(url) => Type.Url -> Raw(url.uri.toString)
- case Content.Text(text) => Type.Text -> Raw(text.string)
- }
-
- def unapply(content: Content): Some[(Type, Raw)] = Some(unpack(content))
- }
- }
-
- @newtype final case class CommentsNr(toNonNegativeInt: Int Refined NonNegative)
- object CommentsNr {
- def unapply(commentsNr: CommentsNr): Some[Int Refined NonNegative] = Some(commentsNr.toNonNegativeInt)
-
- implicit val show: Show[CommentsNr] = Show.wrap(_.toNonNegativeInt.value)
- implicit val order: Order[CommentsNr] = Order.by(_.toNonNegativeInt.value)
- }
-
- @newtype final case class Upvotes(toNonNegativeInt: Int Refined NonNegative)
- object Upvotes {
- def unapply(upvotes: Upvotes): Some[Int Refined NonNegative] = Some(upvotes.toNonNegativeInt)
-
- implicit val show: Show[Upvotes] = Show.wrap(_.toNonNegativeInt.value)
- implicit val order: Order[Upvotes] = Order.by(_.toNonNegativeInt.value)
- }
-
- @newtype final case class Downvotes(toNonNegativeInt: Int Refined NonNegative)
- object Downvotes {
- def unapply(downvotes: Downvotes): Some[Int Refined NonNegative] = Some(downvotes.toNonNegativeInt)
-
- implicit val show: Show[Downvotes] = Show.wrap(_.toNonNegativeInt.value)
- implicit val order: Order[Downvotes] = Order.by(_.toNonNegativeInt.value)
- }
-
- @newtype final case class TotalScore(toInt: Int)
- object TotalScore {
- def unapply(totalScore: TotalScore): Some[Int] = Some(totalScore.toInt)
-
- implicit val show: Show[TotalScore] = Show.wrap(_.toInt)
- implicit val order: Order[TotalScore] = Order.by(_.toInt)
- }
-
- @newtype final case class ControversialScore(toNonNegativeInt: Int Refined NonNegative)
- object ControversialScore {
- def unapply(controversialScore: ControversialScore): Some[Int Refined NonNegative] =
- Some(controversialScore.toNonNegativeInt)
-
- implicit val show: Show[ControversialScore] = Show.wrap(_.toNonNegativeInt.value)
- implicit val order: Order[ControversialScore] = Order.by(_.toNonNegativeInt.value)
- }
-
- sealed trait Sorting extends EnumEntry
- object Sorting extends Enum[Sorting] {
- case object Newest extends Sorting
- case object Hottest extends Sorting
- case object Controversial extends Sorting
-
- val values = findValues
- }
-}
diff --git a/modules/discussions/src/main/scala/io/branchtalk/discussions/model/Subscription.scala b/modules/discussions/src/main/scala/io/branchtalk/discussions/model/Subscription.scala
index 337e4c78..0ffcd0b4 100644
--- a/modules/discussions/src/main/scala/io/branchtalk/discussions/model/Subscription.scala
+++ b/modules/discussions/src/main/scala/io/branchtalk/discussions/model/Subscription.scala
@@ -1,20 +1,39 @@
package io.branchtalk.discussions.model
import io.branchtalk.shared.model.{ FastEq, ID, ShowPretty }
-import io.scalaland.catnip.Semi
-@Semi(FastEq, ShowPretty) final case class Subscription(
+import scala.annotation.targetName
+
+final case class Subscription(
subscriberID: ID[User],
subscriptions: Set[ID[Channel]]
-) {
+) derives FastEq,
+ ShowPretty {
- def ++(subscriptions: Set[ID[Channel]]): Subscription = // scalastyle:ignore method.name
+ @targetName("addAll")
+ def ++(subscriptions: Set[ID[Channel]]): Subscription =
Subscription(subscriberID = subscriberID, subscriptions = this.subscriptions ++ subscriptions)
- def --(subscriptions: Set[ID[Channel]]): Subscription = // scalastyle:ignore method.name
+ @targetName("removeAll")
+ def --(subscriptions: Set[ID[Channel]]): Subscription =
Subscription(subscriberID = subscriberID, subscriptions = this.subscriptions -- subscriptions)
}
-object Subscription extends SubscriptionCommands {
+object Subscription {
+
+ final case class Scheduled(
+ subscription: Subscription
+ ) derives FastEq,
+ ShowPretty
+
+ final case class Subscribe(
+ subscriberID: ID[User],
+ subscriptions: Set[ID[Channel]]
+ ) derives FastEq,
+ ShowPretty
- @Semi(FastEq, ShowPretty) final case class Scheduled(subscription: Subscription)
+ final case class Unsubscribe(
+ subscriberID: ID[User],
+ subscriptions: Set[ID[Channel]]
+ ) derives FastEq,
+ ShowPretty
}
diff --git a/modules/discussions/src/main/scala/io/branchtalk/discussions/model/SubscriptionCommands.scala b/modules/discussions/src/main/scala/io/branchtalk/discussions/model/SubscriptionCommands.scala
deleted file mode 100644
index 0399cef6..00000000
--- a/modules/discussions/src/main/scala/io/branchtalk/discussions/model/SubscriptionCommands.scala
+++ /dev/null
@@ -1,23 +0,0 @@
-package io.branchtalk.discussions.model
-
-import io.branchtalk.shared.model.{ FastEq, ID, ShowPretty }
-import io.scalaland.catnip.Semi
-
-trait SubscriptionCommands {
- type Subscribe = SubscriptionCommands.Subscribe
- type Unsubscribe = SubscriptionCommands.Unsubscribe
- val Subscribe = SubscriptionCommands.Subscribe
- val Unsubscribe = SubscriptionCommands.Unsubscribe
-}
-object SubscriptionCommands {
-
- @Semi(FastEq, ShowPretty) final case class Subscribe(
- subscriberID: ID[User],
- subscriptions: Set[ID[Channel]]
- )
-
- @Semi(FastEq, ShowPretty) final case class Unsubscribe(
- subscriberID: ID[User],
- subscriptions: Set[ID[Channel]]
- )
-}
diff --git a/modules/discussions/src/main/scala/io/branchtalk/discussions/reads/ChannelReads.scala b/modules/discussions/src/main/scala/io/branchtalk/discussions/reads/ChannelReads.scala
index 4c4238a1..9809be88 100644
--- a/modules/discussions/src/main/scala/io/branchtalk/discussions/reads/ChannelReads.scala
+++ b/modules/discussions/src/main/scala/io/branchtalk/discussions/reads/ChannelReads.scala
@@ -1,7 +1,5 @@
package io.branchtalk.discussions.reads
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.numeric.{ NonNegative, Positive }
import io.branchtalk.discussions.model.Channel
import io.branchtalk.shared.model.{ ID, Paginated }
@@ -9,8 +7,8 @@ trait ChannelReads[F[_]] {
def paginate(
sortBy: Channel.Sorting,
- offset: Long Refined NonNegative,
- limit: Int Refined Positive
+ offset: Paginated.Offset,
+ limit: Paginated.Limit
): F[Paginated[Channel]]
def exists(id: ID[Channel]): F[Boolean]
diff --git a/modules/discussions/src/main/scala/io/branchtalk/discussions/reads/CommentReads.scala b/modules/discussions/src/main/scala/io/branchtalk/discussions/reads/CommentReads.scala
index 4527a51a..4f34f375 100644
--- a/modules/discussions/src/main/scala/io/branchtalk/discussions/reads/CommentReads.scala
+++ b/modules/discussions/src/main/scala/io/branchtalk/discussions/reads/CommentReads.scala
@@ -1,7 +1,5 @@
package io.branchtalk.discussions.reads
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.numeric.{ NonNegative, Positive }
import io.branchtalk.discussions.model.{ Comment, Post }
import io.branchtalk.shared.model.{ ID, Paginated }
@@ -11,8 +9,8 @@ trait CommentReads[F[_]] {
post: ID[Post],
repliesTo: Option[ID[Comment]],
sorting: Comment.Sorting,
- offset: Long Refined NonNegative,
- limit: Int Refined Positive
+ offset: Paginated.Offset,
+ limit: Paginated.Limit
): F[Paginated[Comment]]
def exists(id: ID[Comment]): F[Boolean]
diff --git a/modules/discussions/src/main/scala/io/branchtalk/discussions/reads/PostReads.scala b/modules/discussions/src/main/scala/io/branchtalk/discussions/reads/PostReads.scala
index 55a4b0f9..8b73c348 100644
--- a/modules/discussions/src/main/scala/io/branchtalk/discussions/reads/PostReads.scala
+++ b/modules/discussions/src/main/scala/io/branchtalk/discussions/reads/PostReads.scala
@@ -1,8 +1,6 @@
package io.branchtalk.discussions.reads
import cats.data.NonEmptySet
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.numeric.{ NonNegative, Positive }
import io.branchtalk.discussions.model.{ Channel, Post }
import io.branchtalk.shared.model.{ ID, Paginated }
@@ -11,8 +9,8 @@ trait PostReads[F[_]] {
def paginate(
channels: NonEmptySet[ID[Channel]],
sortBy: Post.Sorting,
- offset: Long Refined NonNegative,
- limit: Int Refined Positive
+ offset: Paginated.Offset,
+ limit: Paginated.Limit
): F[Paginated[Post]]
def exists(id: ID[Post]): F[Boolean]
diff --git a/modules/discussions/src/main/scala/io/branchtalk/discussions/writes/PostWrites.scala b/modules/discussions/src/main/scala/io/branchtalk/discussions/writes/PostWrites.scala
index adcdf586..0cd8016b 100644
--- a/modules/discussions/src/main/scala/io/branchtalk/discussions/writes/PostWrites.scala
+++ b/modules/discussions/src/main/scala/io/branchtalk/discussions/writes/PostWrites.scala
@@ -1,7 +1,7 @@
package io.branchtalk.discussions.writes
-import io.branchtalk.discussions.model._
-import io.branchtalk.shared.model._
+import io.branchtalk.discussions.model.*
+import io.branchtalk.shared.model.*
trait PostWrites[F[_]] {
diff --git a/modules/server/src/main/scala/io/branchtalk/api/AppServer.scala b/modules/server/src/main/scala/io/branchtalk/api/AppServer.scala
index 058796b3..b78fd8b1 100644
--- a/modules/server/src/main/scala/io/branchtalk/api/AppServer.scala
+++ b/modules/server/src/main/scala/io/branchtalk/api/AppServer.scala
@@ -3,15 +3,15 @@ package io.branchtalk.api
import cats.arrow.FunctionK
import cats.data.NonEmptyList
import cats.effect.{ Async, Resource }
-import com.softwaremill.macwire.wire
+import fs2.compression.Compression
import io.branchtalk.auth.{ AuthServices, AuthServicesImpl }
import io.branchtalk.configs.{ APIConfig, APIPart, AppArguments, PaginationConfig }
import io.branchtalk.discussions.api.{ ChannelServer, CommentServer, PostServer, SubscriptionServer }
-import io.branchtalk.discussions.reads._
-import io.branchtalk.discussions.writes._
+import io.branchtalk.discussions.reads.*
+import io.branchtalk.discussions.writes.*
import io.branchtalk.logging.MDC
import io.branchtalk.openapi.OpenAPIServer
-import io.branchtalk.shared.model.UUIDGenerator
+import io.branchtalk.shared.model.UUID
import io.branchtalk.users.api.{
ChannelBanServer,
ChannelModerationServer,
@@ -19,22 +19,22 @@ import io.branchtalk.users.api.{
UserModerationServer,
UserServer
}
-import io.branchtalk.users.reads._
-import io.branchtalk.users.writes._
+import io.branchtalk.users.reads.*
+import io.branchtalk.users.writes.*
import io.prometheus.client.CollectorRegistry
-import org.http4s._
+import org.http4s.*
import org.http4s.blaze.server.BlazeServerBuilder
-import org.http4s.implicits._
+import org.http4s.implicits.*
import org.http4s.metrics.MetricsOps
import org.http4s.metrics.prometheus.Prometheus
import org.http4s.server.Server
-import org.http4s.server.middleware._
+import org.http4s.server.middleware.*
import sttp.tapir.server.ServerEndpoint
import scala.annotation.nowarn
final class AppServer[F[_]: Async: MDC](
- usesServer: UserServer[F],
+ userServer: UserServer[F],
userModerationServer: UserModerationServer[F],
channelModerationServer: ChannelModerationServer[F],
userBanServer: UserBanServer[F],
@@ -55,7 +55,7 @@ final class AppServer[F[_]: Async: MDC](
.withAllowCredentials(apiConfig.http.corsAllowCredentials)
.withMaxAge(apiConfig.http.corsMaxAge)
- private val logger = io.branchtalk.shared.model.Logger.getLogger[F]
+ private val logger = io.branchtalk.logging.Logger.getLogger[F]
private val logRoutes = Logger[F, F](
logHeaders = apiConfig.http.logHeaders,
@@ -64,10 +64,12 @@ final class AppServer[F[_]: Async: MDC](
logAction = ((s: String) => logger.info(s)).some
)(_)
+ private given Compression[F] = Compression.forSync[F]
+
val routes: HttpApp[F] =
NonEmptyList
.of(
- usesServer.routes,
+ userServer.routes,
userModerationServer.routes,
channelModerationServer.routes,
userBanServer.routes,
@@ -89,93 +91,107 @@ final class AppServer[F[_]: Async: MDC](
}
object AppServer {
- // scalastyle:off method.length parameter.number
- @nowarn("cat=unused") // macwire
@SuppressWarnings(Array("org.wartremover.warts.GlobalExecutionContext")) // for BlazeServer
def asResource[F[_]: Async: MDC](
- appArguments: AppArguments,
- apiConfig: APIConfig,
- registry: CollectorRegistry,
- userReads: UserReads[F],
- sessionReads: SessionReads[F],
- banReads: BanReads[F],
- userWrites: UserWrites[F],
- sessionWrites: SessionWrites[F],
- banWrites: BanWrites[F],
- channelReads: ChannelReads[F],
- postReads: PostReads[F],
- commentReads: CommentReads[F],
- subscriptionReads: SubscriptionReads[F],
- commentWrites: CommentWrites[F],
- postWrites: PostWrites[F],
- channelWrites: ChannelWrites[F],
- subscriptionWrites: SubscriptionWrites[F]
- )(implicit uuidGenerator: UUIDGenerator): Resource[F, Server] =
+ appArguments: AppArguments,
+ apiConfig: APIConfig,
+ registry: CollectorRegistry,
+ userReads: UserReads[F],
+ sessionReads: SessionReads[F],
+ banReads: BanReads[F],
+ userWrites: UserWrites[F],
+ sessionWrites: SessionWrites[F],
+ banWrites: BanWrites[F],
+ channelReads: ChannelReads[F],
+ postReads: PostReads[F],
+ commentReads: CommentReads[F],
+ subscriptionReads: SubscriptionReads[F],
+ commentWrites: CommentWrites[F],
+ postWrites: PostWrites[F],
+ channelWrites: ChannelWrites[F],
+ subscriptionWrites: SubscriptionWrites[F]
+ )(using UUID.Generator): Resource[F, Server] =
Prometheus.metricsOps[F](registry, "server").flatMap { metricsOps =>
val correlationIDOps: CorrelationIDOps[F] = CorrelationIDOps[F]
val requestIDOps: RequestIDOps[F] = RequestIDOps[F]
- val authServices: AuthServices[F] = wire[AuthServicesImpl[F]]
+ val authServices: AuthServices[F] = AuthServicesImpl[F](userReads, sessionReads, banReads)
- val usersServer: UserServer[F] = {
- val paginationConfig: PaginationConfig = apiConfig.safePagination(APIPart.Users)
- wire[UserServer[F]]
- }
- val userModerationServer: UserModerationServer[F] = {
- val paginationConfig: PaginationConfig = apiConfig.safePagination(APIPart.Users)
- wire[UserModerationServer[F]]
- }
- val channelModerationServer: ChannelModerationServer[F] = {
- val paginationConfig: PaginationConfig = apiConfig.safePagination(APIPart.Users)
- wire[ChannelModerationServer[F]]
- }
- val userBanServer: UserBanServer[F] = wire[UserBanServer[F]]
- val channelBanServer: ChannelBanServer[F] = wire[ChannelBanServer[F]]
- val channelServer: ChannelServer[F] = {
- val paginationConfig: PaginationConfig = apiConfig.safePagination(APIPart.Channels)
- wire[ChannelServer[F]]
- }
- val postServer: PostServer[F] = {
- val paginationConfig: PaginationConfig = apiConfig.safePagination(APIPart.Posts)
- wire[PostServer[F]]
- }
- val commentServer: CommentServer[F] = {
- val paginationConfig: PaginationConfig = apiConfig.safePagination(APIPart.Comments)
- wire[CommentServer[F]]
- }
- val subscriptionServer: SubscriptionServer[F] = {
- val paginationConfig: PaginationConfig = apiConfig.safePagination(APIPart.Posts)
- wire[SubscriptionServer[F]]
- }
- val openAPIServer: OpenAPIServer[F] = {
- import apiConfig.info
- val endpoints: NonEmptyList[ServerEndpoint[Any, F]] =
- NonEmptyList
- .of(
- usersServer.endpoints,
- userModerationServer.endpoints,
- channelModerationServer.endpoints,
- userBanServer.endpoints,
- channelBanServer.endpoints,
- channelServer.endpoints,
- postServer.endpoints,
- commentServer.endpoints,
- subscriptionServer.endpoints
- )
- .reduceK
- wire[OpenAPIServer[F]]
- }
+ val userServer: UserServer[F] = UserServer[F](
+ authServices,
+ userReads,
+ sessionReads,
+ userWrites,
+ sessionWrites,
+ apiConfig.safePagination(APIPart.Users)
+ )
+ val userModerationServer: UserModerationServer[F] =
+ UserModerationServer[F](authServices, userReads, userWrites, apiConfig.safePagination(APIPart.Users))
+ val channelModerationServer: ChannelModerationServer[F] =
+ ChannelModerationServer[F](authServices, userReads, userWrites, apiConfig.safePagination(APIPart.Users))
+ val userBanServer: UserBanServer[F] = UserBanServer[F](authServices, banReads, banWrites)
+ val channelBanServer: ChannelBanServer[F] = ChannelBanServer[F](authServices, banReads, banWrites)
+ val channelServer: ChannelServer[F] =
+ ChannelServer[F](authServices, channelReads, channelWrites, apiConfig.safePagination(APIPart.Channels))
+ val postServer: PostServer[F] =
+ PostServer[F](authServices, postReads, postWrites, apiConfig.safePagination(APIPart.Posts))
+ val commentServer: CommentServer[F] = CommentServer[F](
+ authServices,
+ postReads,
+ commentReads,
+ commentWrites,
+ apiConfig.safePagination(APIPart.Comments)
+ )
+ val subscriptionServer: SubscriptionServer[F] = SubscriptionServer[F](
+ authServices,
+ postReads,
+ subscriptionReads,
+ subscriptionWrites,
+ apiConfig,
+ apiConfig.safePagination(APIPart.Posts)
+ )
+ val openAPIServer: OpenAPIServer[F] = OpenAPIServer[F](
+ apiConfig.info,
+ NonEmptyList
+ .of(
+ userServer.endpoints,
+ userModerationServer.endpoints,
+ channelModerationServer.endpoints,
+ userBanServer.endpoints,
+ channelBanServer.endpoints,
+ channelServer.endpoints,
+ postServer.endpoints,
+ commentServer.endpoints,
+ subscriptionServer.endpoints
+ )
+ .reduceK
+ )
- val appServer = wire[AppServer[F]]
+ val appServer = AppServer[F](
+ userServer,
+ userModerationServer,
+ channelModerationServer,
+ userBanServer,
+ channelBanServer,
+ channelServer,
+ postServer,
+ commentServer,
+ subscriptionServer,
+ openAPIServer,
+ metricsOps,
+ correlationIDOps,
+ requestIDOps,
+ apiConfig
+ )
- val logger = io.branchtalk.shared.model.Logger.getLogger[F]
+ val logger = io.branchtalk.logging.Logger.getLogger[F]
Resource.make(logger.info("Starting up API server"))(_ => logger.info("API server shut down")) >>
BlazeServerBuilder[F]
.enableHttp2(apiConfig.http.http2Enabled)
- .withLengthLimits(maxRequestLineLen = apiConfig.http.maxRequestLineLength.value,
- maxHeadersLen = apiConfig.http.maxHeaderLineLength.value
+ .withLengthLimits(maxRequestLineLen = apiConfig.http.maxRequestLineLength,
+ maxHeadersLen = apiConfig.http.maxHeaderLineLength
)
.bindHttp(port = appArguments.port, host = appArguments.host)
.withHttpApp(appServer.routes)
@@ -184,5 +200,4 @@ object AppServer {
Resource.eval(logger.info(s"API server started at ${server.address.toString}"))
}
}
- // scalastyle:on parameter.number method.length
}
diff --git a/modules/server/src/main/scala/io/branchtalk/api/CorrelationIDOps.scala b/modules/server/src/main/scala/io/branchtalk/api/CorrelationIDOps.scala
index 4bdf9891..1e0657d5 100644
--- a/modules/server/src/main/scala/io/branchtalk/api/CorrelationIDOps.scala
+++ b/modules/server/src/main/scala/io/branchtalk/api/CorrelationIDOps.scala
@@ -3,12 +3,12 @@ package io.branchtalk.api
import cats.data.{ Kleisli, OptionT }
import cats.effect.Sync
import io.branchtalk.logging.{ CorrelationID, MDC }
-import io.branchtalk.shared.model.UUIDGenerator
-import org.http4s._
+import io.branchtalk.shared.model.UUID
+import org.http4s.*
-final class CorrelationIDOps[F[_]: Sync: MDC](implicit uuidGenerator: UUIDGenerator) {
+final class CorrelationIDOps[F[_]: Sync: MDC](using UUID.Generator) {
- def httpRoutes(service: HttpRoutes[F]): HttpRoutes[F] = Kleisli { req: Request[F] =>
+ def httpRoutes(service: HttpRoutes[F]): HttpRoutes[F] = Kleisli { (req: Request[F]) =>
for {
correlationID <- req.headers.get(CorrelationIDOps.correlationIDHeader) match {
case None => CorrelationID.generate[OptionT[F, *]]
@@ -24,5 +24,5 @@ object CorrelationIDOps {
private val correlationIDHeader = org.typelevel.ci.CIString("X-Correlation-ID")
- def apply[F[_]: Sync: MDC](implicit uuidGenerator: UUIDGenerator): CorrelationIDOps[F] = new CorrelationIDOps[F]
+ def apply[F[_]: Sync: MDC](using UUID.Generator): CorrelationIDOps[F] = new CorrelationIDOps[F]
}
diff --git a/modules/server/src/main/scala/io/branchtalk/api/RequestIDOps.scala b/modules/server/src/main/scala/io/branchtalk/api/RequestIDOps.scala
index 72845671..81b1753b 100644
--- a/modules/server/src/main/scala/io/branchtalk/api/RequestIDOps.scala
+++ b/modules/server/src/main/scala/io/branchtalk/api/RequestIDOps.scala
@@ -4,13 +4,13 @@ import cats.data.{ Kleisli, OptionT }
import cats.effect.Sync
import io.branchtalk.logging.{ MDC, RequestID }
import org.http4s.server.middleware.RequestId
-import org.http4s._
+import org.http4s.*
final class RequestIDOps[F[_]: Sync: MDC] {
// reuses RequestId.httpRoutes but adds logging and MDC setup to it
def httpRoutes(service: HttpRoutes[F]): HttpRoutes[F] = RequestId.httpRoutes(
- Kleisli { req: Request[F] =>
+ Kleisli { (req: Request[F]) =>
for {
_ <- req.headers
.get(RequestIDOps.requestIdHeader)
diff --git a/modules/server/src/main/scala/io/branchtalk/api/ServerOptions.scala b/modules/server/src/main/scala/io/branchtalk/api/ServerOptions.scala
index 59123447..34802bb6 100644
--- a/modules/server/src/main/scala/io/branchtalk/api/ServerOptions.scala
+++ b/modules/server/src/main/scala/io/branchtalk/api/ServerOptions.scala
@@ -4,6 +4,7 @@ import cats.effect.Sync
import com.typesafe.scalalogging.Logger
import io.branchtalk.api.JsoniterSupport.JsCodec
import io.branchtalk.api.TapirSupport.jsonBody
+import sttp.monad.MonadError
import sttp.tapir.server.interceptor.decodefailure.{ DecodeFailureHandler, DefaultDecodeFailureHandler }
import sttp.tapir.server.model.ValuedEndpointOutput
import sttp.tapir.server.http4s.Http4sServerOptions
@@ -18,23 +19,29 @@ object ServerOptions {
onMultiple: () => E,
onError: (String, Throwable) => E,
onMismatch: (String, String) => E,
- onValidationError: List[ValidationError[_]] => E
+ onValidationError: List[ValidationError[?]] => E
)
- def buildErrorHandler[E: JsCodec: Schema](errorHandler: ErrorHandler[E]): DecodeFailureHandler = {
- case handled if DefaultDecodeFailureHandler.default.respond(handled).isEmpty =>
- None
- case DecodeFailureContext(_, _, DecodeResult.Missing, _) =>
- Some(ValuedEndpointOutput(jsonBody[E], errorHandler.onMissing()))
- case DecodeFailureContext(_, _, DecodeResult.Multiple(_), _) =>
- Some(ValuedEndpointOutput(jsonBody[E], errorHandler.onMultiple()))
- case DecodeFailureContext(_, _, DecodeResult.Error(original, error), _) =>
- Some(ValuedEndpointOutput(jsonBody[E], errorHandler.onError(original, error)))
- case DecodeFailureContext(_, _, DecodeResult.Mismatch(expected, actual), _) =>
- Some(ValuedEndpointOutput(jsonBody[E], errorHandler.onMismatch(expected, actual)))
- case DecodeFailureContext(_, _, DecodeResult.InvalidValue(errors), _) =>
- Some(ValuedEndpointOutput(jsonBody[E], errorHandler.onValidationError(errors)))
- }
+ def buildErrorHandler[F[_], E: JsCodec: Schema](errorHandler: ErrorHandler[E]): DecodeFailureHandler[F] =
+ new DecodeFailureHandler[F] {
+ def apply(ctx: DecodeFailureContext)(using monad: MonadError[F]): F[Option[ValuedEndpointOutput[?]]] =
+ monad.unit(
+ ctx match {
+ case handled if DefaultDecodeFailureHandler.respond(handled).isEmpty =>
+ None
+ case DecodeFailureContext(_, _, DecodeResult.Missing, _) =>
+ Some(ValuedEndpointOutput(jsonBody[E], errorHandler.onMissing()))
+ case DecodeFailureContext(_, _, DecodeResult.Multiple(_), _) =>
+ Some(ValuedEndpointOutput(jsonBody[E], errorHandler.onMultiple()))
+ case DecodeFailureContext(_, _, DecodeResult.Error(original, error), _) =>
+ Some(ValuedEndpointOutput(jsonBody[E], errorHandler.onError(original, error)))
+ case DecodeFailureContext(_, _, DecodeResult.Mismatch(expected, actual), _) =>
+ Some(ValuedEndpointOutput(jsonBody[E], errorHandler.onMismatch(expected, actual)))
+ case DecodeFailureContext(_, _, DecodeResult.InvalidValue(errors), _) =>
+ Some(ValuedEndpointOutput(jsonBody[E], errorHandler.onValidationError(errors)))
+ }
+ )
+ }
def create[F[_]: Sync, E: JsCodec: Schema](
logger: Logger,
@@ -42,7 +49,7 @@ object ServerOptions {
): Http4sServerOptions[F] =
Http4sServerOptions
.customiseInterceptors[F]
- .decodeFailureHandler(buildErrorHandler(errorHandler))
+ .decodeFailureHandler(buildErrorHandler[F, E](errorHandler))
.serverLog(
DefaultServerLog[F](
doLogWhenReceived = msg => Sync[F].delay(logger.debug(msg)),
diff --git a/modules/server/src/main/scala/io/branchtalk/auth/AuthServices.scala b/modules/server/src/main/scala/io/branchtalk/auth/AuthServices.scala
index 738734ea..7d612a3c 100644
--- a/modules/server/src/main/scala/io/branchtalk/auth/AuthServices.scala
+++ b/modules/server/src/main/scala/io/branchtalk/auth/AuthServices.scala
@@ -4,7 +4,8 @@ import cats.ApplicativeError
import cats.effect.Sync
import com.typesafe.scalalogging.Logger
import io.branchtalk.{ api, users }
-import io.branchtalk.mappings._
+import io.branchtalk.shared.infrastructure.*
+import io.branchtalk.mappings.*
import io.branchtalk.shared.model.{ CodePosition, CommonError, ID }
import io.branchtalk.users.reads.{ BanReads, SessionReads, UserReads }
@@ -20,7 +21,7 @@ trait AuthServices[F[_]] {
}
object AuthServices {
- @inline def apply[F[_]](implicit authServices: AuthServices[F]): AuthServices[F] = authServices
+ inline def apply[F[_]](using authServices: AuthServices[F]): AuthServices[F] = authServices
}
final class AuthServicesImpl[F[_]: Sync](userReads: UserReads[F], sessionReads: SessionReads[F], banReads: BanReads[F])
@@ -74,7 +75,7 @@ final class AuthServicesImpl[F[_]: Sync](userReads: UserReads[F], sessionReads:
val allOwnedPermissions = (allowedChannels ++ allowedOwnProfile).foldLeft(user.data.permissions)(_.append(_))
sessionOpt
.map(_.data.usage)
- .collect { case users.model.SessionProperties.Usage.OAuth(permissions) => permissions }
+ .collect { case users.model.Session.Usage.OAuth(permissions) => permissions }
.fold(allOwnedPermissions)(allOwnedPermissions.intersect)
}
diff --git a/modules/server/src/main/scala/io/branchtalk/auth/auth.scala b/modules/server/src/main/scala/io/branchtalk/auth/auth.scala
new file mode 100644
index 00000000..47d15609
--- /dev/null
+++ b/modules/server/src/main/scala/io/branchtalk/auth/auth.scala
@@ -0,0 +1,93 @@
+package io.branchtalk.auth
+
+import cats.{ Applicative, Functor }
+import io.branchtalk.api.{ Authentication, Authorize, AuthorizeWithOwnership, RequiredPermissions, UserID }
+import io.branchtalk.users.model.{ Session, User }
+
+// no owner, User and Session output
+given authUserSession[F[_]: AuthServices]: Authorize[F, Authentication, (User, Option[Session])] =
+ (auth: Authentication, requiredPermissions: RequiredPermissions) =>
+ AuthServices[F].authorizeUser(auth, requiredPermissions, None)
+given authOptUserSession[F[_]: Applicative: AuthServices]: Authorize[F,
+ Option[Authentication],
+ (Option[User], Option[Session])
+] = (auth: Option[Authentication], requiredPermissions: RequiredPermissions) =>
+ auth.traverse(AuthServices[F].authorizeUser(_, requiredPermissions, None)).map { optUserSession =>
+ optUserSession.map(_._1) -> optUserSession.flatMap(_._2)
+ }
+// no owner, User output
+given authUser[F[_]: Functor: AuthServices]: Authorize[F, Authentication, User] =
+ authUserSession[F].map(_._1)
+given authOptUser[F[_]: Applicative: AuthServices]: Authorize[F, Option[Authentication], Option[User]] =
+ authOptUserSession[F].map(_._1)
+// UserID owner, User and Session output
+given authUserSessionWithUserIDOwnership[F[_]: AuthServices]: AuthorizeWithOwnership[
+ F,
+ Authentication,
+ UserID,
+ (User, Option[Session])
+] =
+ (auth: Authentication, requiredPermissions: RequiredPermissions, owner: UserID) =>
+ AuthServices[F].authorizeUser(auth, requiredPermissions, Some(owner))
+given authOptUserSessionWithUserIDOwnership[F[_]: Applicative: AuthServices]: AuthorizeWithOwnership[
+ F,
+ Option[Authentication],
+ UserID,
+ (Option[User], Option[Session])
+] =
+ (auth: Option[Authentication], requiredPermissions: RequiredPermissions, owner: UserID) =>
+ auth.traverse(AuthServices[F].authorizeUser(_, requiredPermissions, Some(owner))).map { optUserSession =>
+ optUserSession.map(_._1) -> optUserSession.flatMap(_._2)
+ }
+// UserID owner, User output
+given authUserWithUserIDOwnership[F[_]: Functor: AuthServices]: AuthorizeWithOwnership[
+ F,
+ Authentication,
+ UserID,
+ User
+] =
+ authUserSessionWithUserIDOwnership[F].map(_._1)
+given authOptUserWithUserIDOwnership[F[_]: Applicative: AuthServices]: AuthorizeWithOwnership[
+ F,
+ Option[Authentication],
+ UserID,
+ Option[User]
+] =
+ authOptUserSessionWithUserIDOwnership[F].map(_._1)
+// Unit owner, User and Session output
+given authUserSessionWithUnitOwnership[F[_]: AuthServices]: AuthorizeWithOwnership[
+ F,
+ Authentication,
+ Unit,
+ (User, Option[Session])
+] =
+ (auth: Authentication, requiredPermissions: RequiredPermissions, _: Unit) =>
+ AuthServices[F].authorizeUser(auth, requiredPermissions, None)
+implicit def authOptUserSessionWithUnitOwnership[F[_]: Applicative: AuthServices]: AuthorizeWithOwnership[
+ F,
+ Option[Authentication],
+ Unit,
+ (Option[User], Option[Session])
+] =
+ (auth: Option[Authentication], requiredPermissions: RequiredPermissions, _: Unit) =>
+ auth.traverse(AuthServices[F].authorizeUser(_, requiredPermissions, None)).map { optUserSession =>
+ optUserSession.map(_._1) -> optUserSession.flatMap(_._2)
+ }
+// Unit owner, User output
+given authUserWithUnitOwnership[F[_]: Functor: AuthServices]: AuthorizeWithOwnership[
+ F,
+ Authentication,
+ Unit,
+ User
+] =
+ authUserSessionWithUnitOwnership[F].map(_._1)
+given authOptUserWithUnitOwnership[F[_]: Applicative: AuthServices]: AuthorizeWithOwnership[
+ F,
+ Option[Authentication],
+ Unit,
+ Option[User]
+] =
+ (auth: Option[Authentication], requiredPermissions: RequiredPermissions, _: Unit) =>
+ auth.traverse(AuthServices[F].authorizeUser(_, requiredPermissions, None)).map { optUserSession =>
+ optUserSession.map(_._1)
+ }
diff --git a/modules/server/src/main/scala/io/branchtalk/auth/package.scala b/modules/server/src/main/scala/io/branchtalk/auth/package.scala
deleted file mode 100644
index 5b4b27d8..00000000
--- a/modules/server/src/main/scala/io/branchtalk/auth/package.scala
+++ /dev/null
@@ -1,96 +0,0 @@
-package io.branchtalk
-
-import cats.{ Applicative, Functor }
-import io.branchtalk.api.{ Authentication, Authorize, AuthorizeWithOwnership, RequiredPermissions, UserID }
-import io.branchtalk.users.model.{ Session, User }
-
-package object auth {
-
- // no owner, User and Session output
- implicit def authUserSession[F[_]: AuthServices]: Authorize[F, Authentication, (User, Option[Session])] =
- (auth: Authentication, requiredPermissions: RequiredPermissions) =>
- AuthServices[F].authorizeUser(auth, requiredPermissions, None)
- implicit def authOptUserSession[F[_]: Applicative: AuthServices]: Authorize[F, Option[
- Authentication
- ], (Option[User], Option[Session])] =
- (auth: Option[Authentication], requiredPermissions: RequiredPermissions) =>
- auth.traverse(AuthServices[F].authorizeUser(_, requiredPermissions, None)).map { optUserSession =>
- optUserSession.map(_._1) -> optUserSession.flatMap(_._2)
- }
- // no owner, User output
- implicit def authUser[F[_]: Functor: AuthServices]: Authorize[F, Authentication, User] =
- authUserSession[F].map(_._1)
- implicit def authOptUser[F[_]: Applicative: AuthServices]: Authorize[F, Option[Authentication], Option[User]] =
- authOptUserSession[F].map(_._1)
- // UserID owner, User and Session output
- implicit def authUserSessionWithUserIDOwnership[F[_]: AuthServices]: AuthorizeWithOwnership[
- F,
- Authentication,
- UserID,
- (User, Option[Session])
- ] =
- (auth: Authentication, requiredPermissions: RequiredPermissions, owner: UserID) =>
- AuthServices[F].authorizeUser(auth, requiredPermissions, Some(owner))
- implicit def authOptUserSessionWithUserIDOwnership[F[_]: Applicative: AuthServices]: AuthorizeWithOwnership[
- F,
- Option[Authentication],
- UserID,
- (Option[User], Option[Session])
- ] =
- (auth: Option[Authentication], requiredPermissions: RequiredPermissions, owner: UserID) =>
- auth.traverse(AuthServices[F].authorizeUser(_, requiredPermissions, Some(owner))).map { optUserSession =>
- optUserSession.map(_._1) -> optUserSession.flatMap(_._2)
- }
- // UserID owner, User output
- implicit def authUserWithUserIDOwnership[F[_]: Functor: AuthServices]: AuthorizeWithOwnership[
- F,
- Authentication,
- UserID,
- User
- ] =
- authUserSessionWithUserIDOwnership[F].map(_._1)
- implicit def authOptUserWithUserIDOwnership[F[_]: Applicative: AuthServices]: AuthorizeWithOwnership[
- F,
- Option[Authentication],
- UserID,
- Option[User]
- ] =
- authOptUserSessionWithUserIDOwnership[F].map(_._1)
- // Unit owner, User and Session output
- implicit def authUserSessionWithUnitOwnership[F[_]: AuthServices]: AuthorizeWithOwnership[
- F,
- Authentication,
- Unit,
- (User, Option[Session])
- ] =
- (auth: Authentication, requiredPermissions: RequiredPermissions, _: Unit) =>
- AuthServices[F].authorizeUser(auth, requiredPermissions, None)
- implicit def authOptUserSessionWithUnitOwnership[F[_]: Applicative: AuthServices]: AuthorizeWithOwnership[
- F,
- Option[Authentication],
- Unit,
- (Option[User], Option[Session])
- ] =
- (auth: Option[Authentication], requiredPermissions: RequiredPermissions, _: Unit) =>
- auth.traverse(AuthServices[F].authorizeUser(_, requiredPermissions, None)).map { optUserSession =>
- optUserSession.map(_._1) -> optUserSession.flatMap(_._2)
- }
- // Unit owner, User output
- implicit def authUserWithUnitOwnership[F[_]: Functor: AuthServices]: AuthorizeWithOwnership[
- F,
- Authentication,
- Unit,
- User
- ] =
- authUserSessionWithUnitOwnership[F].map(_._1)
- implicit def authOptUserWithUnitOwnership[F[_]: Applicative: AuthServices]: AuthorizeWithOwnership[
- F,
- Option[Authentication],
- Unit,
- Option[User]
- ] =
- (auth: Option[Authentication], requiredPermissions: RequiredPermissions, _: Unit) =>
- auth.traverse(AuthServices[F].authorizeUser(_, requiredPermissions, None)).map { optUserSession =>
- optUserSession.map(_._1)
- }
-}
diff --git a/modules/server/src/main/scala/io/branchtalk/configs/APIConfig.scala b/modules/server/src/main/scala/io/branchtalk/configs/APIConfig.scala
index 252adb24..583b1e2b 100644
--- a/modules/server/src/main/scala/io/branchtalk/configs/APIConfig.scala
+++ b/modules/server/src/main/scala/io/branchtalk/configs/APIConfig.scala
@@ -1,102 +1,95 @@
package io.branchtalk.configs
import cats.Show
-import enumeratum._
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.collection.NonEmpty
-import eu.timepit.refined.numeric.Positive
-import eu.timepit.refined.string.{ MatchesRegex, Url }
-import io.branchtalk.api.{ PaginationLimit, PaginationOffset }
-import io.branchtalk.discussions.model.Channel
-import io.branchtalk.shared.infrastructure.PureconfigSupport._
-import io.branchtalk.shared.model.{ ID, ShowPretty, UUID }
-import io.scalaland.catnip.Semi
+import enumeratum.*
+import io.branchtalk.api.Pagination
+import io.branchtalk.discussions.model.{ Channel, Post }
+import io.branchtalk.users.model.User
+import io.branchtalk.shared.infrastructure.PureconfigSupport.{ *, given }
+import io.branchtalk.shared.model.*
import pureconfig.error.CannotConvert
-import sttp.apispec.openapi._
+import sttp.apispec.openapi.*
+import java.net.URI
import scala.concurrent.duration.FiniteDuration
-@Semi(ConfigReader, ShowPretty) final case class APIContact(
+given ConfigReader[User.Email] = ConfigReader[String].emapString("User.Email")(User.Email.make)
+given ConfigReader[Post.URL] = ConfigReader[URI].emapString("Post.URL")(Post.URL.make)
+given ConfigReader[Paginated.Limit] = ConfigReader[Int].emapString("Paginated.Limit")(Paginated.Limit.make)
+
+final case class APIContact(
name: String,
- email: String Refined MatchesRegex["(.+)@(.+)"],
- url: String Refined Url
-) {
+ email: User.Email,
+ url: Post.URL
+) derives ConfigReader,
+ ShowPretty {
def toOpenAPI: Contact = Contact(
name = name.some,
- email = email.value.some,
- url = url.value.some
+ email = email.unwrap.some,
+ url = url.show.some
)
}
-object APIContact {
- implicit private val showEmail: Show[String Refined MatchesRegex["(.+)@(.+)"]] = _.value
- implicit private val showUrl: Show[String Refined Url] = _.value
-}
-@Semi(ConfigReader, ShowPretty) final case class APILicense(
+final case class APILicense(
name: String,
- url: String Refined Url
-) {
+ url: Post.URL
+) derives ConfigReader,
+ ShowPretty {
def toOpenAPI: License = License(
name = name,
- url = url.value.some
+ url = url.show.some
)
}
-object APILicense {
- implicit private val showUrl: Show[String Refined Url] = _.value
-}
-@Semi(ConfigReader, ShowPretty) final case class APIInfo(
- title: String Refined NonEmpty,
- version: String Refined NonEmpty,
- description: String Refined NonEmpty,
- termsOfService: String Refined Url,
+final case class APIInfo(
+ title: String, // TODO: refine
+ version: String, // TODO: refine
+ description: String, // TODO: refine
+ termsOfService: Post.URL,
contact: APIContact,
license: APILicense
-) {
+) derives ConfigReader,
+ ShowPretty {
def toOpenAPI: Info = Info(
- title = title.value,
- version = version.value,
- description = description.value.some,
- termsOfService = termsOfService.value.some,
+ title = title,
+ version = version,
+ description = description.some,
+ termsOfService = termsOfService.show.some,
contact = contact.toOpenAPI.some,
license = license.toOpenAPI.some
)
}
-object APIInfo {
- implicit private val showNES: Show[String Refined NonEmpty] = _.value
- implicit private val showUrl: Show[String Refined Url] = _.value
-}
-@Semi(ConfigReader, ShowPretty) final case class APIHttp(
+final case class APIHttp(
logHeaders: Boolean,
logBody: Boolean,
http2Enabled: Boolean,
corsAnyOrigin: Boolean,
corsAllowCredentials: Boolean,
corsMaxAge: FiniteDuration,
- maxHeaderLineLength: Int Refined Positive,
- maxRequestLineLength: Int Refined Positive
-)
-object APIHttp {
- implicit private val showPositive: Show[Int Refined Positive] = _.value.toString
-}
-
-@Semi(ConfigReader, ShowPretty) final case class PaginationConfig(
- defaultLimit: PaginationLimit,
- maxLimit: PaginationLimit
-) {
-
- def resolveOffset(passedOffset: Option[PaginationOffset]): PaginationOffset =
- passedOffset.getOrElse(PaginationOffset(0L))
+ maxHeaderLineLength: Int, // TODO: refine
+ maxRequestLineLength: Int // TODO: refine
+) derives ConfigReader,
+ ShowPretty
+
+final case class PaginationConfig(
+ defaultLimit: Paginated.Limit,
+ maxLimit: Paginated.Limit
+) derives ConfigReader,
+ ShowPretty {
+
+ def resolveOffset(passedOffset: Option[Pagination.Offset]): Paginated.Offset =
+ passedOffset.fold(Paginated.Offset(0L)) { value =>
+ Paginated.Offset.unsafeMake(value.unwrap)
+ }
- def resolveLimit(passedLimit: Option[PaginationLimit]): PaginationLimit =
- passedLimit.filter(_.positiveInt.value <= maxLimit.positiveInt.value).getOrElse(defaultLimit)
-}
-object PaginationConfig {
- implicit private val showLimit: Show[PaginationLimit] = _.positiveInt.value.toString
+ def resolveLimit(passedLimit: Option[Pagination.Limit]): Paginated.Limit =
+ passedLimit.filter(_.unwrap <= maxLimit.unwrap).fold(defaultLimit) { value =>
+ Paginated.Limit.unsafeMake(value.unwrap: Int)
+ }
}
sealed trait APIPart extends EnumEntry
@@ -109,7 +102,7 @@ object APIPart extends Enum[APIPart] {
val values: IndexedSeq[APIPart] = findValues
// NOTE: there is no derivation for Map[A, B] ConfigReader, only Map[String, A]
- implicit def asMapKey[A](implicit mapReader: ConfigReader[Map[String, A]]): ConfigReader[Map[APIPart, A]] =
+ given asMapKey[A](using mapReader: ConfigReader[Map[String, A]]): ConfigReader[Map[APIPart, A]] =
mapReader.emap { map =>
map.toList
.traverse { case (key, value) =>
@@ -120,20 +113,23 @@ object APIPart extends Enum[APIPart] {
}
.map(_.toMap)
}
- implicit val show: Show[APIPart] = _.entryName
+ given Show[APIPart] = _.entryName
}
-@Semi(ConfigReader, ShowPretty) final case class APIConfig(
+final case class APIConfig(
info: APIInfo,
http: APIHttp,
defaultChannels: List[UUID],
pagination: Map[APIPart, PaginationConfig]
-) {
+) derives ConfigReader,
+ ShowPretty {
val signedOutSubscriptions: Set[ID[Channel]] = defaultChannels.map(ID[Channel]).toSet
val safePagination: Map[APIPart, PaginationConfig] =
pagination.withDefaultValue(
- PaginationConfig(PaginationLimit(Defaults.defaultPaginationLimit), PaginationLimit(Defaults.maxPaginationLimit))
+ PaginationConfig(Paginated.Limit.unsafeMake(Defaults.defaultPaginationLimit),
+ Paginated.Limit.unsafeMake(Defaults.maxPaginationLimit)
+ )
)
}
diff --git a/modules/server/src/main/scala/io/branchtalk/configs/AppArguments.scala b/modules/server/src/main/scala/io/branchtalk/configs/AppArguments.scala
index 4b1be499..5f6fd96e 100644
--- a/modules/server/src/main/scala/io/branchtalk/configs/AppArguments.scala
+++ b/modules/server/src/main/scala/io/branchtalk/configs/AppArguments.scala
@@ -1,32 +1,29 @@
package io.branchtalk.configs
import cats.effect.{ ExitCode, Sync }
-import com.monovore.decline._
+import com.monovore.decline.*
import com.typesafe.config.{ Config, ConfigRenderOptions }
import io.branchtalk.shared.model.ShowPretty
-import io.scalaland.catnip.Semi
-@Semi(ShowPretty) final case class AppArguments(
+final case class AppArguments(
host: String = Defaults.host,
port: Int = Defaults.port,
runAPI: Boolean = Defaults.runAPI,
runUsersProjections: Boolean = Defaults.runUsersProjections,
runDiscussionsProjections: Boolean = Defaults.runDiscussionsProjections
-) {
+) derives ShowPretty {
def isAnythingRun: Boolean = runAPI || runUsersProjections || runDiscussionsProjections
}
object AppArguments {
- implicit private class BoolOps[A](private val opts: Opts[A]) extends AnyVal {
+ extension [A](opts: Opts[A]) {
- def orBool(bool: Boolean)(implicit isUnit: A <:< Unit): Opts[Boolean] =
- if (bool) opts.orTrue else opts.orFalse
+ private def orBool(bool: Boolean)(using A <:< Unit): Opts[Boolean] = if (bool) opts.orTrue else opts.orFalse
}
final case class NoConfig(help: Help) extends Exception {
- // scalastyle:off regex
def printHelp(config: Config): ExitCode = {
println(help.toString())
println(additionalInfo(config))
@@ -38,7 +35,6 @@ object AppArguments {
println(help.errors.map(" " + _).intercalate("\n"))
ExitCode.Error
}
- // scalastyle:on regex
}
private val help: Opts[Nothing] =
diff --git a/modules/server/src/main/scala/io/branchtalk/configs/Configuration.scala b/modules/server/src/main/scala/io/branchtalk/configs/Configuration.scala
index 0727f8f2..bb5e502e 100644
--- a/modules/server/src/main/scala/io/branchtalk/configs/Configuration.scala
+++ b/modules/server/src/main/scala/io/branchtalk/configs/Configuration.scala
@@ -2,7 +2,7 @@ package io.branchtalk.configs
import cats.data.NonEmptyList
import cats.effect.Sync
-import io.branchtalk.shared.infrastructure.PureconfigSupport._
+import io.branchtalk.shared.infrastructure.PureconfigSupport.{ *, given }
import java.io.File
import scala.reflect.ClassTag
diff --git a/modules/server/src/main/scala/io/branchtalk/configs/Defaults.scala b/modules/server/src/main/scala/io/branchtalk/configs/Defaults.scala
index 4eac6ee0..e65ffd9c 100644
--- a/modules/server/src/main/scala/io/branchtalk/configs/Defaults.scala
+++ b/modules/server/src/main/scala/io/branchtalk/configs/Defaults.scala
@@ -1,8 +1,5 @@
package io.branchtalk.configs
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.numeric.Positive
-
object Defaults {
val host: String = "localhost"
@@ -12,6 +9,6 @@ object Defaults {
val runUsersProjections: Boolean = false
val runDiscussionsProjections: Boolean = false
- val defaultPaginationLimit: Int Refined Positive = 50
- val maxPaginationLimit: Int Refined Positive = 100
+ val defaultPaginationLimit: Int = 50
+ val maxPaginationLimit: Int = 100
}
diff --git a/modules/server/src/main/scala/io/branchtalk/configs/package.scala b/modules/server/src/main/scala/io/branchtalk/configs/package.scala
deleted file mode 100644
index 503c767a..00000000
--- a/modules/server/src/main/scala/io/branchtalk/configs/package.scala
+++ /dev/null
@@ -1,12 +0,0 @@
-package io.branchtalk
-
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.numeric.Positive
-import io.branchtalk.api.PaginationLimit
-import io.branchtalk.shared.infrastructure.PureconfigSupport._
-
-package object configs {
-
- implicit val paginationLimitReader: ConfigReader[PaginationLimit] =
- ConfigReader[Int Refined Positive].map(PaginationLimit(_))
-}
diff --git a/modules/server/src/main/scala/io/branchtalk/configs/paginations.scala b/modules/server/src/main/scala/io/branchtalk/configs/paginations.scala
new file mode 100644
index 00000000..0dd670ee
--- /dev/null
+++ b/modules/server/src/main/scala/io/branchtalk/configs/paginations.scala
@@ -0,0 +1,7 @@
+package io.branchtalk.configs
+
+import io.branchtalk.api.Pagination
+import io.branchtalk.shared.infrastructure.PureconfigSupport.{ *, given }
+
+given paginationLimitReader: ConfigReader[Pagination.Limit] =
+ ConfigReader[Int].emapString("Pagination.Limit")(Pagination.Limit.make)
diff --git a/modules/server/src/main/scala/io/branchtalk/discussions/api/ChannelServer.scala b/modules/server/src/main/scala/io/branchtalk/discussions/api/ChannelServer.scala
index 0e6b275c..c3a29b56 100644
--- a/modules/server/src/main/scala/io/branchtalk/discussions/api/ChannelServer.scala
+++ b/modules/server/src/main/scala/io/branchtalk/discussions/api/ChannelServer.scala
@@ -3,19 +3,20 @@ package io.branchtalk.discussions.api
import cats.data.NonEmptyList
import cats.effect.{ Async, Sync }
import com.typesafe.scalalogging.Logger
-import io.branchtalk.api._
-import io.branchtalk.auth._
+import io.branchtalk.api.*
+import io.branchtalk.auth.{ *, given }
import io.branchtalk.configs.PaginationConfig
-import io.branchtalk.discussions.api.ChannelModels._
+import io.branchtalk.discussions.api.ChannelModels.*
import io.branchtalk.discussions.model.Channel
import io.branchtalk.discussions.reads.ChannelReads
import io.branchtalk.discussions.writes.ChannelWrites
import io.branchtalk.mappings.userIDUsers2Discussions
+import io.branchtalk.shared.infrastructure.*
import io.branchtalk.shared.model.{ CommonError, CreationScheduled }
import io.branchtalk.users.model.User
-import io.scalaland.chimney.dsl._
-import org.http4s._
-import sttp.tapir.server.http4s._
+import io.scalaland.chimney.dsl.*
+import org.http4s.*
+import sttp.tapir.server.http4s.*
import sttp.tapir.server.ServerEndpoint
final class ChannelServer[F[_]: Async](
@@ -25,20 +26,20 @@ final class ChannelServer[F[_]: Async](
paginationConfig: PaginationConfig
) {
- implicit private val as: AuthServices[F] = authServices
+ private given AuthServices[F] = authServices
private val logger = Logger(getClass)
- implicit val serverOptions: Http4sServerOptions[F] = ChannelServer.serverOptions[F].apply(logger)
+ private given serverOptions: Http4sServerOptions[F] = ChannelServer.serverOptions[F](logger)
- implicit private val errorHandler: ServerErrorHandler[F, ChannelError] = ChannelServer.errorHandler[F].apply(logger)
+ private given errorHandler: ServerErrorHandler[F, ChannelError] = ChannelServer.errorHandler[F](logger)
private val paginate = ChannelAPIs.paginate.serverLogic[F, Option[User]] { case (optOffset, optLimit) =>
val sortBy = Channel.Sorting.Newest
val offset = paginationConfig.resolveOffset(optOffset)
val limit = paginationConfig.resolveLimit(optLimit)
for {
- paginated <- channelReads.paginate(sortBy, offset.nonNegativeLong, limit.positiveInt)
+ paginated <- channelReads.paginate(sortBy, offset, limit)
} yield Pagination.fromPaginated(paginated.map(APIChannel.fromDomain), offset, limit)
}
@@ -98,7 +99,7 @@ final class ChannelServer[F[_]: Async](
}
object ChannelServer {
- def serverOptions[F[_]: Sync]: Logger => Http4sServerOptions[F] = ServerOptions.create[F, ChannelError](
+ def serverOptions[F[_]](using Sync[F]): Logger => Http4sServerOptions[F] = ServerOptions.create[F, ChannelError](
_,
ServerOptions.ErrorHandler[ChannelError](
() => ChannelError.ValidationFailed(NonEmptyList.one("Data missing")),
@@ -114,7 +115,7 @@ object ChannelServer {
)
)
- def errorHandler[F[_]: Sync]: Logger => ServerErrorHandler[F, ChannelError] =
+ def errorHandler[F[_]](using Sync[F]): Logger => ServerErrorHandler[F, ChannelError] =
ServerErrorHandler.handleCommonErrors[F, ChannelError] {
case CommonError.InvalidCredentials(_) =>
ChannelError.BadCredentials("Invalid credentials")
diff --git a/modules/server/src/main/scala/io/branchtalk/discussions/api/CommentServer.scala b/modules/server/src/main/scala/io/branchtalk/discussions/api/CommentServer.scala
index f1dbf407..b1d8fd27 100644
--- a/modules/server/src/main/scala/io/branchtalk/discussions/api/CommentServer.scala
+++ b/modules/server/src/main/scala/io/branchtalk/discussions/api/CommentServer.scala
@@ -3,19 +3,20 @@ package io.branchtalk.discussions.api
import cats.data.NonEmptyList
import cats.effect.{ Async, Sync }
import com.typesafe.scalalogging.Logger
-import io.branchtalk.api._
-import io.branchtalk.auth._
+import io.branchtalk.api.*
+import io.branchtalk.auth.{ *, given }
import io.branchtalk.configs.PaginationConfig
-import io.branchtalk.discussions.api.CommentModels._
+import io.branchtalk.discussions.api.CommentModels.*
import io.branchtalk.discussions.model.{ Channel, Comment, Post }
import io.branchtalk.discussions.reads.{ CommentReads, PostReads }
import io.branchtalk.discussions.writes.CommentWrites
-import io.branchtalk.mappings._
+import io.branchtalk.mappings.*
+import io.branchtalk.shared.infrastructure.*
import io.branchtalk.shared.model.{ CommonError, CreationScheduled, ID }
import io.branchtalk.users.model.User
-import io.scalaland.chimney.dsl._
-import org.http4s._
-import sttp.tapir.server.http4s._
+import io.scalaland.chimney.dsl.*
+import org.http4s.*
+import sttp.tapir.server.http4s.*
import sttp.tapir.server.ServerEndpoint
final class CommentServer[F[_]: Async](
@@ -26,13 +27,13 @@ final class CommentServer[F[_]: Async](
paginationConfig: PaginationConfig
) {
- implicit private val as: AuthServices[F] = authServices
+ private given AuthServices[F] = authServices
private val logger = Logger(getClass)
- private val serverOptions: Http4sServerOptions[F] = CommentServer.serverOptions[F].apply(logger)
+ private val serverOptions: Http4sServerOptions[F] = CommentServer.serverOptions[F](logger)
- implicit private val errorHandler: ServerErrorHandler[F, CommentError] = CommentServer.errorHandler[F].apply(logger)
+ private given errorHandler: ServerErrorHandler[F, CommentError] = CommentServer.errorHandler[F](logger)
private def testOwnership(channelID: ID[Channel], postID: ID[Post]) = postReads
.requireById(postID)
@@ -73,7 +74,7 @@ final class CommentServer[F[_]: Async](
val offset = paginationConfig.resolveOffset(optOffset)
val limit = paginationConfig.resolveLimit(optLimit)
for {
- paginated <- commentReads.paginate(postID, optReply, sortBy, offset.nonNegativeLong, limit.positiveInt)
+ paginated <- commentReads.paginate(postID, optReply, sortBy, offset, limit)
} yield Pagination.fromPaginated(paginated.map(APIComment.fromDomain), offset, limit)
}
@@ -84,7 +85,7 @@ final class CommentServer[F[_]: Async](
val offset = paginationConfig.resolveOffset(None)
val limit = paginationConfig.resolveLimit(None)
for {
- paginated <- commentReads.paginate(postID, optReply, sortBy, offset.nonNegativeLong, limit.positiveInt)
+ paginated <- commentReads.paginate(postID, optReply, sortBy, offset, limit)
} yield Pagination.fromPaginated(paginated.map(APIComment.fromDomain), offset, limit)
}
@@ -95,7 +96,7 @@ final class CommentServer[F[_]: Async](
val offset = paginationConfig.resolveOffset(None)
val limit = paginationConfig.resolveLimit(None)
for {
- paginated <- commentReads.paginate(postID, optReply, sortBy, offset.nonNegativeLong, limit.positiveInt)
+ paginated <- commentReads.paginate(postID, optReply, sortBy, offset, limit)
} yield Pagination.fromPaginated(paginated.map(APIComment.fromDomain), offset, limit)
}
@@ -217,7 +218,7 @@ final class CommentServer[F[_]: Async](
}
object CommentServer {
- def serverOptions[F[_]: Sync]: Logger => Http4sServerOptions[F] = ServerOptions.create[F, CommentError](
+ def serverOptions[F[_]](using Sync[F]): Logger => Http4sServerOptions[F] = ServerOptions.create[F, CommentError](
_,
ServerOptions.ErrorHandler[CommentError](
() => CommentError.ValidationFailed(NonEmptyList.one("Data missing")),
@@ -233,7 +234,7 @@ object CommentServer {
)
)
- def errorHandler[F[_]: Sync]: Logger => ServerErrorHandler[F, CommentError] =
+ def errorHandler[F[_]](using Sync[F]): Logger => ServerErrorHandler[F, CommentError] =
ServerErrorHandler.handleCommonErrors[F, CommentError] {
case CommonError.InvalidCredentials(_) =>
CommentError.BadCredentials("Invalid credentials")
diff --git a/modules/server/src/main/scala/io/branchtalk/discussions/api/PostServer.scala b/modules/server/src/main/scala/io/branchtalk/discussions/api/PostServer.scala
index 13499706..73b456fb 100644
--- a/modules/server/src/main/scala/io/branchtalk/discussions/api/PostServer.scala
+++ b/modules/server/src/main/scala/io/branchtalk/discussions/api/PostServer.scala
@@ -3,19 +3,20 @@ package io.branchtalk.discussions.api
import cats.data.{ NonEmptyList, NonEmptySet }
import cats.effect.{ Async, Sync }
import com.typesafe.scalalogging.Logger
-import io.branchtalk.api._
-import io.branchtalk.auth._
+import io.branchtalk.api.*
+import io.branchtalk.auth.{ *, given }
import io.branchtalk.configs.PaginationConfig
-import io.branchtalk.discussions.api.PostModels._
+import io.branchtalk.discussions.api.PostModels.*
import io.branchtalk.discussions.model.{ Channel, Post }
import io.branchtalk.discussions.reads.PostReads
import io.branchtalk.discussions.writes.PostWrites
-import io.branchtalk.mappings._
+import io.branchtalk.mappings.*
+import io.branchtalk.shared.infrastructure.*
import io.branchtalk.shared.model.{ CommonError, CreationScheduled, ID, Paginated }
import io.branchtalk.users.model.User
-import io.scalaland.chimney.dsl._
-import org.http4s._
-import sttp.tapir.server.http4s._
+import io.scalaland.chimney.dsl.*
+import org.http4s.*
+import sttp.tapir.server.http4s.*
import sttp.tapir.server.ServerEndpoint
import scala.collection.immutable.SortedSet
@@ -27,13 +28,13 @@ final class PostServer[F[_]: Async](
paginationConfig: PaginationConfig
) {
- implicit private val as: AuthServices[F] = authServices
+ private given AuthServices[F] = authServices
private val logger = Logger(getClass)
- private val serverOptions: Http4sServerOptions[F] = PostServer.serverOptions[F].apply(logger)
+ private val serverOptions: Http4sServerOptions[F] = PostServer.serverOptions[F](logger)
- implicit private val errorHandler: ServerErrorHandler[F, PostError] = PostServer.errorHandler[F].apply(logger)
+ private given errorHandler: ServerErrorHandler[F, PostError] = PostServer.errorHandler[F](logger)
private def testOwnership(channelID: ID[Channel], postID: ID[Post], isDeleted: Boolean = false) = postReads
.requireById(postID, isDeleted)
@@ -53,7 +54,7 @@ final class PostServer[F[_]: Async](
val channelIDS = SortedSet(channelID)
for {
paginated <- NonEmptySet.fromSet(channelIDS) match {
- case Some(channelIDs) => postReads.paginate(channelIDs, sortBy, offset.nonNegativeLong, limit.positiveInt)
+ case Some(channelIDs) => postReads.paginate(channelIDs, sortBy, offset, limit)
case None => Paginated.empty[Post].pure[F]
}
} yield Pagination.fromPaginated(paginated.map(APIPost.fromDomain), offset, limit)
@@ -66,7 +67,7 @@ final class PostServer[F[_]: Async](
val channelIDS = SortedSet(channelID)
for {
paginated <- NonEmptySet.fromSet(channelIDS) match {
- case Some(channelIDs) => postReads.paginate(channelIDs, sortBy, offset.nonNegativeLong, limit.positiveInt)
+ case Some(channelIDs) => postReads.paginate(channelIDs, sortBy, offset, limit)
case None => Paginated.empty[Post].pure[F]
}
} yield Pagination.fromPaginated(paginated.map(APIPost.fromDomain), offset, limit)
@@ -79,7 +80,7 @@ final class PostServer[F[_]: Async](
val channelIDS = SortedSet(channelID)
for {
paginated <- NonEmptySet.fromSet(channelIDS) match {
- case Some(channelIDs) => postReads.paginate(channelIDs, sortBy, offset.nonNegativeLong, limit.positiveInt)
+ case Some(channelIDs) => postReads.paginate(channelIDs, sortBy, offset, limit)
case None => Paginated.empty[Post].pure[F]
}
} yield Pagination.fromPaginated(paginated.map(APIPost.fromDomain), offset, limit)
@@ -199,7 +200,7 @@ final class PostServer[F[_]: Async](
}
object PostServer {
- def serverOptions[F[_]: Sync]: Logger => Http4sServerOptions[F] = ServerOptions.create[F, PostError](
+ def serverOptions[F[_]](using Sync[F]): Logger => Http4sServerOptions[F] = ServerOptions.create[F, PostError](
_,
ServerOptions.ErrorHandler[PostError](
() => PostError.ValidationFailed(NonEmptyList.one("Data missing")),
@@ -215,7 +216,7 @@ object PostServer {
)
)
- def errorHandler[F[_]: Sync]: Logger => ServerErrorHandler[F, PostError] =
+ def errorHandler[F[_]](using Sync[F]): Logger => ServerErrorHandler[F, PostError] =
ServerErrorHandler.handleCommonErrors[F, PostError] {
case CommonError.InvalidCredentials(_) =>
PostError.BadCredentials("Invalid credentials")
diff --git a/modules/server/src/main/scala/io/branchtalk/discussions/api/SubscriptionServer.scala b/modules/server/src/main/scala/io/branchtalk/discussions/api/SubscriptionServer.scala
index a49e499d..569042df 100644
--- a/modules/server/src/main/scala/io/branchtalk/discussions/api/SubscriptionServer.scala
+++ b/modules/server/src/main/scala/io/branchtalk/discussions/api/SubscriptionServer.scala
@@ -3,19 +3,20 @@ package io.branchtalk.discussions.api
import cats.data.{ NonEmptyList, NonEmptySet }
import cats.effect.{ Async, Sync }
import com.typesafe.scalalogging.Logger
-import io.branchtalk.api._
-import io.branchtalk.auth._
+import io.branchtalk.api.*
+import io.branchtalk.auth.{ *, given }
import io.branchtalk.configs.{ APIConfig, PaginationConfig }
-import io.branchtalk.discussions.api.PostModels._
-import io.branchtalk.discussions.api.SubscriptionModels._
+import io.branchtalk.discussions.api.PostModels.*
+import io.branchtalk.discussions.api.SubscriptionModels.*
import io.branchtalk.discussions.model.{ Post, Subscription }
import io.branchtalk.discussions.reads.{ PostReads, SubscriptionReads }
import io.branchtalk.discussions.writes.SubscriptionWrites
-import io.branchtalk.mappings._
+import io.branchtalk.mappings.*
+import io.branchtalk.shared.infrastructure.*
import io.branchtalk.shared.model.{ CommonError, Paginated }
import io.branchtalk.users.model.User
-import org.http4s._
-import sttp.tapir.server.http4s._
+import org.http4s.*
+import sttp.tapir.server.http4s.*
import sttp.tapir.server.ServerEndpoint
import scala.collection.immutable.SortedSet
@@ -29,16 +30,15 @@ final class SubscriptionServer[F[_]: Async](
paginationConfig: PaginationConfig
) {
- implicit private val as: AuthServices[F] = authServices
+ private given AuthServices[F] = authServices
private val logger = Logger(getClass)
- private val serverOptions: Http4sServerOptions[F] = SubscriptionServer.serverOptions[F].apply(logger)
+ private val serverOptions: Http4sServerOptions[F] = SubscriptionServer.serverOptions[F](logger)
- implicit private val postErrorHandler: ServerErrorHandler[F, PostError] = PostServer.errorHandler[F].apply(logger)
+ private given postErrorHandler: ServerErrorHandler[F, PostError] = PostServer.errorHandler[F](logger)
- implicit private val errorHandler: ServerErrorHandler[F, SubscriptionError] =
- SubscriptionServer.errorHandler[F].apply(logger)
+ private given errorHandler: ServerErrorHandler[F, SubscriptionError] = SubscriptionServer.errorHandler[F](logger)
private val newest =
SubscriptionAPIs.newest.serverLogic[F, Option[User]].withUser { case (optUser, (optOffset, optLimit)) =>
@@ -49,7 +49,7 @@ final class SubscriptionServer[F[_]: Async](
subscriptionOpt <- optUser.map(_.id).map(userIDUsers2Discussions.get).traverse(subscriptionReads.requireForUser)
channelIDS = SortedSet.from(subscriptionOpt.map(_.subscriptions).getOrElse(apiConfig.signedOutSubscriptions))
paginated <- NonEmptySet.fromSet(channelIDS) match {
- case Some(channelIDs) => postReads.paginate(channelIDs, sortBy, offset.nonNegativeLong, limit.positiveInt)
+ case Some(channelIDs) => postReads.paginate(channelIDs, sortBy, offset, limit)
case None => Paginated.empty[Post].pure[F]
}
} yield Pagination.fromPaginated(paginated.map(APIPost.fromDomain), offset, limit)
@@ -88,7 +88,7 @@ final class SubscriptionServer[F[_]: Async](
}
object SubscriptionServer {
- def serverOptions[F[_]: Sync]: Logger => Http4sServerOptions[F] =
+ def serverOptions[F[_]](using Sync[F]): Logger => Http4sServerOptions[F] =
ServerOptions.create[F, SubscriptionError](
_,
ServerOptions.ErrorHandler[SubscriptionError](
@@ -106,7 +106,7 @@ object SubscriptionServer {
)
)
- def errorHandler[F[_]: Sync]: Logger => ServerErrorHandler[F, SubscriptionError] =
+ def errorHandler[F[_]](using Sync[F]): Logger => ServerErrorHandler[F, SubscriptionError] =
ServerErrorHandler.handleCommonErrors[F, SubscriptionError] {
case CommonError.InvalidCredentials(_) =>
SubscriptionError.BadCredentials("Invalid credentials")
diff --git a/modules/server/src/main/scala/io/branchtalk/mappings/mappings.scala b/modules/server/src/main/scala/io/branchtalk/mappings/mappings.scala
new file mode 100644
index 00000000..8dff76a4
--- /dev/null
+++ b/modules/server/src/main/scala/io/branchtalk/mappings/mappings.scala
@@ -0,0 +1,120 @@
+package io.branchtalk.mappings
+
+import cats.data.NonEmptySet
+import io.branchtalk.shared.model.ID
+import io.branchtalk.{ api, discussions, users }
+
+import scala.collection.immutable.SortedSet
+import scala.util.Try
+
+class Iso[A, B](val get: A => B)(val reverseGet: B => A)
+
+// API <-> Users
+
+val usernameApi2Users: Iso[api.Username, users.model.User.Name] = Iso[api.Username, users.model.User.Name] { username =>
+ users.model.User.Name.unsafeMake(username.unwrap)
+}(username => api.Username.unsafeMake(username.unwrap))
+
+val passwordApi2Users: Iso[api.Password, users.model.Password.Raw] = Iso[api.Password, users.model.Password.Raw] {
+ password => users.model.Password.Raw.unsafeMake(password.unwrap)
+}(password => api.Password.unsafeMake(password.unwrap))
+
+val sessionIDApi2Users: Iso[api.SessionID, ID[users.model.Session]] = Iso[api.SessionID, ID[users.model.Session]] {
+ sessionID => ID[users.model.Session](sessionID.unwrap)
+}(sessionID => api.SessionID(sessionID.unwrap))
+
+val userIDApi2Users: Iso[api.UserID, ID[users.model.User]] = Iso[api.UserID, ID[users.model.User]] { userID =>
+ ID[users.model.User](userID.unwrap)
+}(userID => api.UserID(userID.unwrap))
+
+val channelIDApi2Users: Iso[api.ChannelID, ID[users.model.Channel]] = Iso[api.ChannelID, ID[users.model.Channel]] {
+ channelID => ID[users.model.Channel](channelID.unwrap)
+}(channelID => api.ChannelID(channelID.unwrap))
+
+@SuppressWarnings(Array("org.wartremover.warts.Throw")) // too PITA to do it right
+def permissionApi2Users(owner: api.UserID): Iso[api.Permission, users.model.Permission] =
+ Iso[api.Permission, users.model.Permission] {
+ case api.Permission.Administrate =>
+ users.model.Permission.Administrate
+ case api.Permission.IsOwner =>
+ users.model.Permission.IsUser(userIDApi2Users.get(owner))
+ case api.Permission.ModerateUsers =>
+ users.model.Permission.ModerateUsers
+ case api.Permission.ModerateChannel(channelID) =>
+ users.model.Permission.ModerateChannel(channelIDApi2Users.get(channelID))
+ case api.Permission.CanPublish(channelID) =>
+ users.model.Permission.CanPublish(channelIDApi2Users.get(channelID))
+ } {
+ case users.model.Permission.Administrate =>
+ api.Permission.Administrate
+ case users.model.Permission.IsUser(userID) if userID.unwrap === owner.unwrap && owner =!= api.UserID.empty =>
+ api.Permission.IsOwner
+ case users.model.Permission.IsUser(_) =>
+ throw new Exception("Cannot map User to Owner if ID doesn't match current Owner ID")
+ case users.model.Permission.ModerateUsers =>
+ api.Permission.ModerateUsers
+ case users.model.Permission.ModerateChannel(channelID) =>
+ api.Permission.ModerateChannel(channelIDApi2Users.reverseGet(channelID))
+ case users.model.Permission.CanPublish(channelID) =>
+ api.Permission.CanPublish(channelIDApi2Users.reverseGet(channelID))
+ }
+
+def requiredPermissionsApi2Users(owner: api.UserID): Iso[api.RequiredPermissions, users.model.RequiredPermissions] = {
+ val permApi2Users = permissionApi2Users(owner)
+ def safeReverseGet(perm: users.model.Permission) =
+ Try(SortedSet(permApi2Users.reverseGet(perm))).getOrElse(SortedSet.empty[api.Permission])
+ lazy val reqApi2Users: Iso[api.RequiredPermissions, users.model.RequiredPermissions] =
+ Iso[api.RequiredPermissions, users.model.RequiredPermissions] {
+ case api.RequiredPermissions.Empty =>
+ users.model.RequiredPermissions.Empty
+ case api.RequiredPermissions.AllOf(set) =>
+ users.model.RequiredPermissions.AllOf(set.map(permApi2Users.get))
+ case api.RequiredPermissions.AnyOf(set) =>
+ users.model.RequiredPermissions.AnyOf(set.map(permApi2Users.get))
+ case api.RequiredPermissions.And(x, y) =>
+ users.model.RequiredPermissions.And(reqApi2Users.get(x), reqApi2Users.get(y))
+ case api.RequiredPermissions.Or(x, y) =>
+ users.model.RequiredPermissions.Or(reqApi2Users.get(x), reqApi2Users.get(y))
+ case api.RequiredPermissions.Not(x) => users.model.RequiredPermissions.Not(reqApi2Users.get(x))
+ } {
+ case users.model.RequiredPermissions.Empty =>
+ api.RequiredPermissions.Empty
+ case users.model.RequiredPermissions.AllOf(set) =>
+ NonEmptySet
+ .fromSet(set.toSortedSet.flatMap(safeReverseGet))
+ .fold[api.RequiredPermissions](api.RequiredPermissions.Empty)(api.RequiredPermissions.AllOf)
+ case users.model.RequiredPermissions.AnyOf(set) =>
+ NonEmptySet
+ .fromSet(set.toSortedSet.flatMap(safeReverseGet))
+ .fold[api.RequiredPermissions](api.RequiredPermissions.Empty)(api.RequiredPermissions.AnyOf)
+ case users.model.RequiredPermissions.And(x, y) =>
+ api.RequiredPermissions.And(reqApi2Users.reverseGet(x), reqApi2Users.reverseGet(y))
+ case users.model.RequiredPermissions.Or(x, y) =>
+ api.RequiredPermissions.Or(reqApi2Users.reverseGet(x), reqApi2Users.reverseGet(y))
+ case users.model.RequiredPermissions.Not(x) => api.RequiredPermissions.Not(reqApi2Users.reverseGet(x))
+ }
+ reqApi2Users
+}
+
+// API <-> Discussions
+
+val userIDApi2Discussions: Iso[api.UserID, ID[discussions.model.User]] = Iso[api.UserID, ID[discussions.model.User]] {
+ userID => ID[discussions.model.User](userID.unwrap)
+}(id => api.UserID(id.unwrap))
+
+val channelIDApi2Discussions: Iso[api.ChannelID, ID[discussions.model.Channel]] =
+ Iso[api.ChannelID, ID[discussions.model.Channel]](userID => ID[discussions.model.Channel](userID.unwrap))(id =>
+ api.ChannelID(id.unwrap)
+ )
+
+// Users <-> Discussions
+
+val userIDUsers2Discussions: Iso[ID[users.model.User], ID[discussions.model.User]] =
+ Iso[ID[users.model.User], ID[discussions.model.User]](userID => ID[discussions.model.User](userID.unwrap)) { userID =>
+ ID[users.model.User](userID.unwrap)
+ }
+
+val channelIDUsers2Discussions: Iso[ID[users.model.Channel], ID[discussions.model.Channel]] =
+ Iso[ID[users.model.Channel], ID[discussions.model.Channel]] { channelID =>
+ ID[discussions.model.Channel](channelID.unwrap)
+ }(channelID => ID[users.model.Channel](channelID.unwrap))
diff --git a/modules/server/src/main/scala/io/branchtalk/mappings/package.scala b/modules/server/src/main/scala/io/branchtalk/mappings/package.scala
deleted file mode 100644
index 8b166112..00000000
--- a/modules/server/src/main/scala/io/branchtalk/mappings/package.scala
+++ /dev/null
@@ -1,126 +0,0 @@
-package io.branchtalk
-
-import cats.data.NonEmptySet
-import io.branchtalk.api.UserID
-import io.branchtalk.shared.model.ID
-import monocle.Iso
-
-import scala.collection.immutable.SortedSet
-import scala.util.Try
-
-package object mappings {
-
- // API <-> Users
-
- val usernameApi2Users: Iso[api.Username, users.model.User.Name] = Iso[api.Username, users.model.User.Name] {
- username => users.model.User.Name(username.nonEmptyString)
- }(username => api.Username(username.nonEmptyString))
-
- val passwordApi2Users: Iso[api.Password, users.model.Password.Raw] = Iso[api.Password, users.model.Password.Raw] {
- password => users.model.Password.Raw(password.nonEmptyBytes)
- }(password => api.Password(password.nonEmptyBytes))
-
- val sessionIDApi2Users: Iso[api.SessionID, ID[users.model.Session]] = Iso[api.SessionID, ID[users.model.Session]] {
- sessionID => ID[users.model.Session](sessionID.uuid)
- }(sessionID => api.SessionID(sessionID.uuid))
-
- val userIDApi2Users: Iso[api.UserID, ID[users.model.User]] = Iso[api.UserID, ID[users.model.User]] { userID =>
- ID[users.model.User](userID.uuid)
- }(userID => api.UserID(userID.uuid))
-
- val channelIDApi2Users: Iso[api.ChannelID, ID[users.model.Channel]] = Iso[api.ChannelID, ID[users.model.Channel]] {
- channelID => ID[users.model.Channel](channelID.uuid)
- }(channelID => api.ChannelID(channelID.uuid))
-
- // scalastyle:off cyclomatic.complexity
- @SuppressWarnings(Array("org.wartremover.warts.Throw")) // too PITA to do it right
- def permissionApi2Users(owner: UserID): Iso[api.Permission, users.model.Permission] =
- Iso[api.Permission, users.model.Permission] {
- case api.Permission.Administrate =>
- users.model.Permission.Administrate
- case api.Permission.IsOwner =>
- users.model.Permission.IsUser(userIDApi2Users.get(owner))
- case api.Permission.ModerateUsers =>
- users.model.Permission.ModerateUsers
- case api.Permission.ModerateChannel(channelID) =>
- users.model.Permission.ModerateChannel(channelIDApi2Users.get(channelID))
- case api.Permission.CanPublish(channelID) =>
- users.model.Permission.CanPublish(channelIDApi2Users.get(channelID))
- } {
- case users.model.Permission.Administrate =>
- api.Permission.Administrate
- case users.model.Permission.IsUser(userID) if userID.uuid === owner.uuid && owner =!= UserID.empty =>
- api.Permission.IsOwner
- case users.model.Permission.IsUser(_) =>
- throw new Exception("Cannot map User to Owner if ID doesn't match current Owner ID")
- case users.model.Permission.ModerateUsers =>
- api.Permission.ModerateUsers
- case users.model.Permission.ModerateChannel(channelID) =>
- api.Permission.ModerateChannel(channelIDApi2Users.reverseGet(channelID))
- case users.model.Permission.CanPublish(channelID) =>
- api.Permission.CanPublish(channelIDApi2Users.reverseGet(channelID))
- }
- // scalastyle:on cyclomatic.complexity
-
- // scalastyle:off cyclomatic.complexity
- def requiredPermissionsApi2Users(owner: UserID): Iso[api.RequiredPermissions, users.model.RequiredPermissions] = {
- val permApi2Users = permissionApi2Users(owner)
- def safeReverseGet(perm: users.model.Permission) =
- Try(SortedSet(permApi2Users.reverseGet(perm))).getOrElse(SortedSet.empty[api.Permission])
- lazy val reqApi2Users: Iso[api.RequiredPermissions, users.model.RequiredPermissions] =
- Iso[api.RequiredPermissions, users.model.RequiredPermissions] {
- case api.RequiredPermissions.Empty =>
- users.model.RequiredPermissions.Empty
- case api.RequiredPermissions.AllOf(set) =>
- users.model.RequiredPermissions.AllOf(set.map(permApi2Users.get))
- case api.RequiredPermissions.AnyOf(set) =>
- users.model.RequiredPermissions.AnyOf(set.map(permApi2Users.get))
- case api.RequiredPermissions.And(x, y) =>
- users.model.RequiredPermissions.And(reqApi2Users.get(x), reqApi2Users.get(y))
- case api.RequiredPermissions.Or(x, y) =>
- users.model.RequiredPermissions.Or(reqApi2Users.get(x), reqApi2Users.get(y))
- case api.RequiredPermissions.Not(x) => users.model.RequiredPermissions.Not(reqApi2Users.get(x))
- } {
- case users.model.RequiredPermissions.Empty =>
- api.RequiredPermissions.Empty
- case users.model.RequiredPermissions.AllOf(set) =>
- NonEmptySet
- .fromSet(set.toSortedSet.flatMap(safeReverseGet))
- .fold[api.RequiredPermissions](api.RequiredPermissions.Empty)(api.RequiredPermissions.AllOf)
- case users.model.RequiredPermissions.AnyOf(set) =>
- NonEmptySet
- .fromSet(set.toSortedSet.flatMap(safeReverseGet))
- .fold[api.RequiredPermissions](api.RequiredPermissions.Empty)(api.RequiredPermissions.AnyOf)
- case users.model.RequiredPermissions.And(x, y) =>
- api.RequiredPermissions.And(reqApi2Users.reverseGet(x), reqApi2Users.reverseGet(y))
- case users.model.RequiredPermissions.Or(x, y) =>
- api.RequiredPermissions.Or(reqApi2Users.reverseGet(x), reqApi2Users.reverseGet(y))
- case users.model.RequiredPermissions.Not(x) => api.RequiredPermissions.Not(reqApi2Users.reverseGet(x))
- }
- reqApi2Users
- }
- // scalastyle:on cyclomatic.complexity
-
- // API <-> Discussions
-
- val userIDApi2Discussions: Iso[api.UserID, ID[discussions.model.User]] = Iso[api.UserID, ID[discussions.model.User]] {
- userID => ID[discussions.model.User](userID.uuid)
- }(id => api.UserID(id.uuid))
-
- val channelIDApi2Discussions: Iso[api.ChannelID, ID[discussions.model.Channel]] =
- Iso[api.ChannelID, ID[discussions.model.Channel]](userID => ID[discussions.model.Channel](userID.uuid))(id =>
- api.ChannelID(id.uuid)
- )
-
- // Users <-> Discussions
-
- val userIDUsers2Discussions: Iso[ID[users.model.User], ID[discussions.model.User]] =
- Iso[ID[users.model.User], ID[discussions.model.User]](userID => ID[discussions.model.User](userID.uuid)) { userID =>
- ID[users.model.User](userID.uuid)
- }
-
- val channelIDUsers2Discussions: Iso[ID[users.model.Channel], ID[discussions.model.Channel]] =
- Iso[ID[users.model.Channel], ID[discussions.model.Channel]] { channelID =>
- ID[discussions.model.Channel](channelID.uuid)
- }(channelID => ID[users.model.Channel](channelID.uuid))
-}
diff --git a/modules/server/src/main/scala/io/branchtalk/openapi/OpenAPIServer.scala b/modules/server/src/main/scala/io/branchtalk/openapi/OpenAPIServer.scala
index 37c7c901..df1e4887 100644
--- a/modules/server/src/main/scala/io/branchtalk/openapi/OpenAPIServer.scala
+++ b/modules/server/src/main/scala/io/branchtalk/openapi/OpenAPIServer.scala
@@ -2,13 +2,12 @@ package io.branchtalk.openapi
import cats.data.NonEmptyList
import cats.effect.Sync
-import com.github.plokhotnyuk.jsoniter_scala.core._
-import com.github.plokhotnyuk.jsoniter_scala.macros._
+import com.github.plokhotnyuk.jsoniter_scala.core.*
+import com.github.plokhotnyuk.jsoniter_scala.macros.*
import io.branchtalk.api
-import io.branchtalk.api.JsoniterSupport._
+import io.branchtalk.api.JsoniterSupport.*
import io.branchtalk.configs.APIInfo
import org.http4s.HttpRoutes
-import monocle.macros.syntax.lens._
import sttp.apispec.{
Discriminator,
ExampleMultipleValue,
@@ -18,16 +17,17 @@ import sttp.apispec.{
ExternalDocumentation,
OAuthFlow,
OAuthFlows,
- Reference,
- ReferenceOr,
+ Pattern,
Schema,
+ SchemaLike,
SchemaType,
SecurityRequirement,
SecurityScheme,
Tag
}
-import sttp.apispec.openapi._
-import sttp.tapir.docs.openapi._
+import sttp.apispec.openapi.ReferenceOr
+import sttp.apispec.openapi.*
+import sttp.tapir.docs.openapi.*
import sttp.tapir.server.ServerEndpoint
import sttp.tapir.swagger.http4s.SwaggerHttp4s
@@ -39,16 +39,18 @@ final class OpenAPIServer[F[_]: Sync](
endpoints: NonEmptyList[ServerEndpoint[Any, F]]
) {
- import OpenAPIServer._
+ import OpenAPIServer.{ *, given }
private val removedName = classOf[api.RequiredPermissions].getName
private def fixPathItem(pathItem: PathItem) =
- pathItem.focus(_.parameters).modify(_.filterNot(_.fold(_.$ref.contains(removedName), _.name.contains(removedName))))
+ pathItem.copy(parameters =
+ pathItem.parameters.filterNot(_.fold(_.$ref.contains(removedName), _.name.contains(removedName)))
+ )
def openAPI: OpenAPI = OpenAPIDocsInterpreter(OpenAPIServer.openAPIDocsOptions)
.toOpenAPI(endpoints.map(_.endpoint).toList, apiInfo.toOpenAPI)
- .focus(_.paths.pathItems)
- .modify(_.view.mapValues(fixPathItem).to(ListMap))
+ // TODO: quicklens
+ .pipe(oa => oa.copy(paths = oa.paths.copy(pathItems = oa.paths.pathItems.view.mapValues(fixPathItem).to(ListMap))))
val openAPIJson: String = writeToString(openAPI)
@@ -57,19 +59,19 @@ final class OpenAPIServer[F[_]: Sync](
@SuppressWarnings(Array("org.wartremover.warts.All")) // macros
object OpenAPIServer {
- implicit private val openAPIDocsOptions: OpenAPIDocsOptions = OpenAPIDocsOptions.default
+ private given openAPIDocsOptions: OpenAPIDocsOptions = OpenAPIDocsOptions.default
// technically, we only need encoder part so we can mock all the rest and call it a day
trait JsEncoderOnly[T] extends JsCodec[T] {
- override def decodeValue(in: JsonReader, default: T): T = ???
- override def nullValue: T = null.asInstanceOf[T] // scalastyle:ignore null
- def encodeValue(x: T, out: JsonWriter): Unit
+ override def decodeValue(in: JsonReader, default: T): T = ???
+ override def nullValue: T = null.asInstanceOf[T]
+ def encodeValue(x: T, out: JsonWriter): Unit
}
object JsEncoderOnly {
def apply[T](f: (T, JsonWriter) => Unit): JsEncoderOnly[T] = (value: T, out: JsonWriter) => f(value, out)
}
- implicit val encoderReference: JsCodec[Reference] = JsonCodecMaker.make
+ given encoderReference: JsCodec[Reference] = JsonCodecMaker.make
// apparently Jsoniter cannot find this ...
def encoderReferenceOr[T: JsCodec]: JsCodec[ReferenceOr[T]] =
JsEncoderOnly[ReferenceOr[T]] { (x, out) =>
@@ -77,100 +79,100 @@ object OpenAPIServer {
}
// so I have to apply this manually
@nowarn("msg=Implicit resolves to enclosing value") // here this is just because of recursion
- implicit lazy val encoderReferenceOrSchema: JsCodec[ReferenceOr[Schema]] = encoderReferenceOr
- implicit lazy val encoderReferenceOrParameterCodec: JsCodec[ReferenceOr[Parameter]] = encoderReferenceOr
- implicit lazy val encoderReferenceOrRequestBodyCodec: JsCodec[ReferenceOr[RequestBody]] = encoderReferenceOr
- implicit lazy val encoderReferenceOrResponseCodec: JsCodec[ReferenceOr[Response]] = encoderReferenceOr
- implicit lazy val encoderReferenceOrExampleCodec: JsCodec[ReferenceOr[Example]] = encoderReferenceOr
- implicit lazy val encoderReferenceOrHeaderCodec: JsCodec[ReferenceOr[Header]] = encoderReferenceOr
+ given encoderReferenceOrSchema: JsCodec[ReferenceOr[Schema]] = encoderReferenceOr
+ given encoderReferenceOrParameterCodec: JsCodec[ReferenceOr[Parameter]] = encoderReferenceOr
+ given encoderReferenceOrRequestBodyCodec: JsCodec[ReferenceOr[RequestBody]] = encoderReferenceOr
+ given encoderReferenceOrResponseCodec: JsCodec[ReferenceOr[Response]] = encoderReferenceOr
+ given encoderReferenceOrExampleCodec: JsCodec[ReferenceOr[Example]] = encoderReferenceOr
+ given encoderReferenceOrHeaderCodec: JsCodec[ReferenceOr[Header]] = encoderReferenceOr
// TODO: support extension at all
- implicit val extensionValue: JsCodec[ExtensionValue] = JsEncoderOnly[ExtensionValue] { (x, out) =>
+ given extensionValue: JsCodec[ExtensionValue] = JsEncoderOnly[ExtensionValue] { (x, out) =>
JsonCodecMaker.make[String].encodeValue(x.toString, out)
}
- implicit val encoderOAuthFlow: JsCodec[OAuthFlow] = JsonCodecMaker.make
- implicit val encoderOAuthFlows: JsCodec[OAuthFlows] = JsonCodecMaker.make
- implicit val encoderSecurityScheme: JsCodec[SecurityScheme] = JsonCodecMaker.make
- implicit val encoderExampleSingleValue: JsCodec[ExampleSingleValue] = JsEncoderOnly {
+ given encoderOAuthFlow: JsCodec[OAuthFlow] = JsonCodecMaker.make
+ given encoderOAuthFlows: JsCodec[OAuthFlows] = JsonCodecMaker.make
+ given encoderSecurityScheme: JsCodec[SecurityScheme] = JsonCodecMaker.make
+ given encoderExampleSingleValue: JsCodec[ExampleSingleValue] = JsEncoderOnly {
// TODO: handle parse -> encode JSON
- case (ExampleSingleValue(value: String), out) => JsonCodecMaker.make[String].encodeValue(value, out)
- case (ExampleSingleValue(value: Int), out) => JsonCodecMaker.make[Int].encodeValue(value, out)
- case (ExampleSingleValue(value: Long), out) => JsonCodecMaker.make[Long].encodeValue(value, out)
- case (ExampleSingleValue(value: Float), out) => JsonCodecMaker.make[Float].encodeValue(value, out)
- case (ExampleSingleValue(value: Double), out) => JsonCodecMaker.make[Double].encodeValue(value, out)
- case (ExampleSingleValue(value: Boolean), out) => JsonCodecMaker.make[Boolean].encodeValue(value, out)
+ case (ExampleSingleValue(value: String), out) => JsonCodecMaker.make[String].encodeValue(value, out)
+ case (ExampleSingleValue(value: Int), out) => JsonCodecMaker.make[Int].encodeValue(value, out)
+ case (ExampleSingleValue(value: Long), out) => JsonCodecMaker.make[Long].encodeValue(value, out)
+ case (ExampleSingleValue(value: Float), out) => JsonCodecMaker.make[Float].encodeValue(value, out)
+ case (ExampleSingleValue(value: Double), out) => JsonCodecMaker.make[Double].encodeValue(value, out)
+ case (ExampleSingleValue(value: Boolean), out) => JsonCodecMaker.make[Boolean].encodeValue(value, out)
case (ExampleSingleValue(value: BigDecimal), out) => JsonCodecMaker.make[BigDecimal].encodeValue(value, out)
- case (ExampleSingleValue(value: BigInt), out) => JsonCodecMaker.make[BigInt].encodeValue(value, out)
- case (ExampleSingleValue(null), out) => // scalastyle:ignore null
- JsonCodecMaker.make[Option[String]].encodeValue(None, out)
- case (ExampleSingleValue(value), out) => JsonCodecMaker.make[String].encodeValue(value.toString, out)
+ case (ExampleSingleValue(value: BigInt), out) => JsonCodecMaker.make[BigInt].encodeValue(value, out)
+ case (ExampleSingleValue(null), out) => JsonCodecMaker.make[Option[String]].encodeValue(None, out)
+ case (ExampleSingleValue(value), out) => JsonCodecMaker.make[String].encodeValue(value.toString, out)
}
val encodeExampleMultipleValues: JsCodec[ExampleMultipleValue] =
- summonCodec[List[ExampleSingleValue]](JsonCodecMaker.make).map[ExampleMultipleValue](_ => ???) {
+ JsonCodecMaker.make[List[ExampleSingleValue]].map[ExampleMultipleValue](_ => ???) {
case ExampleMultipleValue(values) => values.map(ExampleSingleValue)
}
- implicit val encodeExampleValue: JsCodec[ExampleValue] = JsEncoderOnly[ExampleValue] {
- case (e: ExampleSingleValue, out) => encoderExampleSingleValue.encodeValue(e, out)
+ given encodeExampleValue: JsCodec[ExampleValue] = JsEncoderOnly[ExampleValue] {
+ case (e: ExampleSingleValue, out) => encoderExampleSingleValue.encodeValue(e, out)
case (e: ExampleMultipleValue, out) => encodeExampleMultipleValues.encodeValue(e, out)
}
- implicit val encoderSchemaType: JsCodec[SchemaType] = summonCodec[String](JsonCodecMaker.make).map(_ => ???)(_.value)
- implicit val encoderSchema: JsCodec[Schema] = JsonCodecMaker.make
- implicit val encoderHeader: JsCodec[Header] = JsonCodecMaker.make
- implicit val encoderExample: JsCodec[Example] = JsonCodecMaker.make
- implicit val encoderResponse: JsCodec[Response] = JsonCodecMaker.make
- implicit val encoderLink: JsCodec[Link] = JsonCodecMaker.make
- implicit lazy val encoderCallback: JsCodec[Callback] =
+ given encoderPattern: JsonKeyCodec[Pattern] = new JsonKeyCodec[Pattern] {
+ private val impl = JsonCodecMaker.make[String]
+ override def decodeKey(in: JsonReader): Pattern = Pattern(impl.decodeValue(in, ""))
+ override def encodeKey(x: Pattern, out: JsonWriter): Unit = impl.encodeValue(x.value, out)
+ }
+ given encoderSchemaType: JsCodec[SchemaType] = JsonCodecMaker.make[String].map(_ => ???)(_.value)
+ given encoderSchemaLike: JsCodec[SchemaLike] = JsonCodecMaker.make
+ given encoderSchema: JsCodec[Schema] = JsonCodecMaker.makeWithoutDiscriminator
+ given encoderHeader: JsCodec[Header] = JsonCodecMaker.make
+ given encoderExample: JsCodec[Example] = JsonCodecMaker.make
+ given encoderResponse: JsCodec[Response] = JsonCodecMaker.make
+ given encoderLink: JsCodec[Link] = JsonCodecMaker.make
+ given encoderCallback: JsCodec[Callback] =
encodeListMap(encoderReferenceOr[PathItem]).map[Callback](_ => ???)(_.pathItems)
- implicit val encoderEncoding: JsCodec[Encoding] = JsonCodecMaker.make
- implicit val encoderMediaType: JsCodec[MediaType] = JsonCodecMaker.make
- implicit val encoderRequestBody: JsCodec[RequestBody] = JsonCodecMaker.make
- implicit val encoderParameterStyle: JsCodec[ParameterStyle] =
- summonCodec[String](JsonCodecMaker.make).map(_ => ???)(_.value)
- implicit val encoderParameterIn: JsCodec[ParameterIn] = JsonCodecMaker.make
- implicit val encoderParameter: JsCodec[Parameter] = JsonCodecMaker.make
- implicit val encoderResponseMap: JsCodec[ListMap[ResponsesKey, ReferenceOr[Response]]] =
- summonCodec[Map[String, ReferenceOr[Response]]](
- JsonCodecMaker.make(CodecMakerConfig.withAllowRecursiveTypes(true))
- ).map[ListMap[ResponsesKey, ReferenceOr[Response]]](_ => ???)(
- _.map {
- case (ResponsesDefaultKey, r) => ("default", r)
- case (ResponsesCodeKey(code), r) => (code.toString, r)
- case (ResponsesRangeKey(range), r) => (s"${range}XX", r)
- }
- )
+ given encoderEncoding: JsCodec[Encoding] = JsonCodecMaker.make
+ given encoderMediaType: JsCodec[MediaType] = JsonCodecMaker.make
+ given encoderRequestBody: JsCodec[RequestBody] = JsonCodecMaker.make
+ given encoderParameterStyle: JsCodec[ParameterStyle] = JsonCodecMaker.make[String].map(_ => ???)(_.value)
+ given encoderParameterIn: JsCodec[ParameterIn] = JsonCodecMaker.make
+ given encoderParameter: JsCodec[Parameter] = JsonCodecMaker.make
+ given encoderResponseMap: JsCodec[ListMap[ResponsesKey, ReferenceOr[Response]]] =
+ JsonCodecMaker
+ .make[Map[String, ReferenceOr[Response]]](CodecMakerConfig.withAllowRecursiveTypes(true))
+ .map[ListMap[ResponsesKey, ReferenceOr[Response]]](_ => ???)(
+ _.map {
+ case (ResponsesDefaultKey, r) => ("default", r)
+ case (ResponsesCodeKey(code), r) => (code.toString, r)
+ case (ResponsesRangeKey(range), r) => (s"${range}XX", r)
+ }
+ )
// TODO: handle extensions one day
- implicit val encoderResponses: JsCodec[Responses] = encoderResponseMap.map[Responses](_ => ???) {
+ given encoderResponses: JsCodec[Responses] = encoderResponseMap.map[Responses](_ => ???) {
case Responses(responses, _) => responses
}
// this is needed to override the encoding of `security: List[SecurityRequirement]`. An empty security requirement
// should be represented as an empty object (`{}`), not `null`, which is the default encoding of `ListMap`s.
- implicit def encodeSecurityRequirement: JsCodec[List[SecurityRequirement]] =
+ given encodeSecurityRequirement: JsCodec[List[SecurityRequirement]] =
JsonCodecMaker.make(CodecMakerConfig.withAllowRecursiveTypes(true).withTransientEmpty(true))
- implicit val operationCodec: JsCodec[Operation] = JsonCodecMaker.make(CodecMakerConfig.withAllowRecursiveTypes(true))
- implicit val encoderPathItem: JsCodec[PathItem] = JsonCodecMaker.make
- implicit val encoderPaths: JsCodec[Paths] =
- summonCodec[ListMap[String, PathItem]](JsonCodecMaker.make).map(_ => ???) { case Paths(pathItems, _) =>
- pathItems
- }
- implicit val encoderComponents: JsCodec[Components] = JsonCodecMaker.make
- implicit val encoderServerVariable: JsCodec[ServerVariable] = JsonCodecMaker.make
- implicit val encoderServer: JsCodec[Server] = JsonCodecMaker.make
- implicit val encoderExternalDocumentation: JsCodec[ExternalDocumentation] = JsonCodecMaker.make
- implicit val encoderTag: JsCodec[Tag] = JsonCodecMaker.make
- implicit val encoderInfo: JsCodec[Info] = JsonCodecMaker.make
- implicit val encoderContact: JsCodec[Contact] = JsonCodecMaker.make
- implicit val encoderLicense: JsCodec[License] = JsonCodecMaker.make
- implicit val encoderOpenAPI: JsCodec[OpenAPI] =
+ given operationCodec: JsCodec[Operation] = JsonCodecMaker.make(CodecMakerConfig.withAllowRecursiveTypes(true))
+ given encoderPathItem: JsCodec[PathItem] = JsonCodecMaker.make
+ given encoderPaths: JsCodec[Paths] =
+ JsonCodecMaker.make[ListMap[String, PathItem]].map(_ => ???) { case Paths(pathItems, _) => pathItems }
+ given encoderComponents: JsCodec[Components] = JsonCodecMaker.make
+ given encoderServerVariable: JsCodec[ServerVariable] = JsonCodecMaker.make
+ given encoderServer: JsCodec[Server] = JsonCodecMaker.make
+ given encoderExternalDocumentation: JsCodec[ExternalDocumentation] = JsonCodecMaker.make
+ given encoderTag: JsCodec[Tag] = JsonCodecMaker.make
+ given encoderInfo: JsCodec[Info] = JsonCodecMaker.make
+ given encoderContact: JsCodec[Contact] = JsonCodecMaker.make
+ given encoderLicense: JsCodec[License] = JsonCodecMaker.make
+ given encoderOpenAPI: JsCodec[OpenAPI] =
JsonCodecMaker.make(CodecMakerConfig.withTransientDefault(false).withTransientNone(true))
- implicit val encoderDiscriminator: JsCodec[Discriminator] = JsonCodecMaker.make
+ given encoderDiscriminator: JsCodec[Discriminator] = JsonCodecMaker.make
- implicit def encodeList[T: JsCodec]: JsCodec[List[T]] = JsEncoderOnly[List[T]] {
- case (Nil, out) =>
- summonCodec[Option[T]](JsonCodecMaker.make(CodecMakerConfig.withTransientNone(false))).encodeValue(None, out)
- case (list, out) =>
- summonCodec[Vector[T]](JsonCodecMaker.make).encodeValue(list.toVector, out)
+ given encodeList[T: JsCodec]: JsCodec[List[T]] = JsEncoderOnly[List[T]] {
+ case (Nil, out) => JsonCodecMaker.make[Option[T]](CodecMakerConfig.withTransientNone(false)).encodeValue(None, out)
+ case (list, out) => JsonCodecMaker.make[Vector[T]].encodeValue(list.toVector, out)
}
- implicit def encodeListMap[V: JsCodec]: JsCodec[ListMap[String, V]] =
+ given encodeListMap[V: JsCodec]: JsCodec[ListMap[String, V]] =
JsonCodecMaker.make(CodecMakerConfig.withTransientEmpty(false))
}
diff --git a/modules/server/src/main/scala/io/branchtalk/users/api/ChannelBanServer.scala b/modules/server/src/main/scala/io/branchtalk/users/api/ChannelBanServer.scala
index 6165a712..f164ddf6 100644
--- a/modules/server/src/main/scala/io/branchtalk/users/api/ChannelBanServer.scala
+++ b/modules/server/src/main/scala/io/branchtalk/users/api/ChannelBanServer.scala
@@ -3,15 +3,16 @@ package io.branchtalk.users.api
import cats.data.NonEmptyList
import cats.effect.{ Async, Sync }
import com.typesafe.scalalogging.Logger
-import io.branchtalk.api.{ Permission => _, _ }
-import io.branchtalk.auth._
+import io.branchtalk.api.{ Permission => _, * }
+import io.branchtalk.auth.{ *, given }
+import io.branchtalk.shared.infrastructure.*
import io.branchtalk.shared.model.CommonError
-import io.branchtalk.users.api.UserModels._
+import io.branchtalk.users.api.UserModels.*
import io.branchtalk.users.model.{ Ban, User }
import io.branchtalk.users.reads.BanReads
import io.branchtalk.users.writes.BanWrites
-import org.http4s._
-import sttp.tapir.server.http4s._
+import org.http4s.*
+import sttp.tapir.server.http4s.*
import sttp.tapir.server.ServerEndpoint
final class ChannelBanServer[F[_]: Async](
@@ -20,14 +21,14 @@ final class ChannelBanServer[F[_]: Async](
banWrites: BanWrites[F]
) {
- implicit private val as: AuthServices[F] = authServices
+ private given AuthServices[F] = authServices
private val logger = Logger(getClass)
- private val serverOptions: Http4sServerOptions[F] = ChannelBanServer.serverOptions[F].apply(logger)
+ private val serverOptions: Http4sServerOptions[F] = ChannelBanServer.serverOptions[F](logger)
- implicit private val errorHandler: ServerErrorHandler[F, UserError] =
- ChannelBanServer.errorHandler[F].apply(logger)
+ private given errorHandler: ServerErrorHandler[F, UserError] =
+ ChannelBanServer.errorHandler[F](logger)
private val list = ChannelBanAPIs.list.serverLogic[F, User] { channelID =>
for {
@@ -62,7 +63,7 @@ final class ChannelBanServer[F[_]: Async](
}
object ChannelBanServer {
- def serverOptions[F[_]: Sync]: Logger => Http4sServerOptions[F] = ServerOptions.create[F, UserError](
+ def serverOptions[F[_]](using Sync[F]): Logger => Http4sServerOptions[F] = ServerOptions.create[F, UserError](
_,
ServerOptions.ErrorHandler[UserError](
() => UserError.ValidationFailed(NonEmptyList.one("Data missing")),
@@ -78,7 +79,7 @@ object ChannelBanServer {
)
)
- def errorHandler[F[_]: Sync]: Logger => ServerErrorHandler[F, UserError] =
+ def errorHandler[F[_]](using Sync[F]): Logger => ServerErrorHandler[F, UserError] =
ServerErrorHandler.handleCommonErrors[F, UserError] {
case CommonError.InvalidCredentials(_) =>
UserError.BadCredentials("Invalid credentials")
diff --git a/modules/server/src/main/scala/io/branchtalk/users/api/ChannelModerationServer.scala b/modules/server/src/main/scala/io/branchtalk/users/api/ChannelModerationServer.scala
index 5ea1a6dd..d6b8811a 100644
--- a/modules/server/src/main/scala/io/branchtalk/users/api/ChannelModerationServer.scala
+++ b/modules/server/src/main/scala/io/branchtalk/users/api/ChannelModerationServer.scala
@@ -3,16 +3,17 @@ package io.branchtalk.users.api
import cats.data.NonEmptyList
import cats.effect.{ Async, Sync }
import com.typesafe.scalalogging.Logger
-import io.branchtalk.api.{ Permission => _, _ }
-import io.branchtalk.auth._
+import io.branchtalk.api.{ Permission => _, * }
+import io.branchtalk.auth.{ *, given }
import io.branchtalk.configs.PaginationConfig
+import io.branchtalk.shared.infrastructure.*
import io.branchtalk.shared.model.{ CommonError, OptionUpdatable, Updatable }
-import io.branchtalk.users.api.UserModels._
+import io.branchtalk.users.api.UserModels.*
import io.branchtalk.users.model.{ Permission, User }
import io.branchtalk.users.reads.UserReads
import io.branchtalk.users.writes.UserWrites
-import org.http4s._
-import sttp.tapir.server.http4s._
+import org.http4s.*
+import sttp.tapir.server.http4s.*
import sttp.tapir.server.ServerEndpoint
final class ChannelModerationServer[F[_]: Async](
@@ -22,14 +23,13 @@ final class ChannelModerationServer[F[_]: Async](
paginationConfig: PaginationConfig
) {
- implicit private val as: AuthServices[F] = authServices
+ private given AuthServices[F] = authServices
private val logger = Logger(getClass)
- private val serverOptions: Http4sServerOptions[F] = ChannelModerationServer.serverOptions[F].apply(logger)
+ private val serverOptions: Http4sServerOptions[F] = ChannelModerationServer.serverOptions[F](logger)
- implicit private val errorHandler: ServerErrorHandler[F, UserError] =
- ChannelModerationServer.errorHandler[F].apply(logger)
+ private given errorHandler: ServerErrorHandler[F, UserError] = ChannelModerationServer.errorHandler[F](logger)
private val paginate =
ChannelModerationAPIs.paginate.serverLogic[F, User] { case (channelID, optOffset, optLimit) =>
@@ -38,7 +38,7 @@ final class ChannelModerationServer[F[_]: Async](
val limit = paginationConfig.resolveLimit(optLimit)
val filters = List(User.Filter.HasPermission(Permission.ModerateChannel(channelID)))
for {
- paginated <- userReads.paginate(sortBy, offset.nonNegativeLong, limit.positiveInt, filters)
+ paginated <- userReads.paginate(sortBy, offset, limit, filters)
} yield Pagination.fromPaginated(paginated.map(APIUser.fromDomain), offset, limit)
}
@@ -83,7 +83,7 @@ final class ChannelModerationServer[F[_]: Async](
object ChannelModerationServer {
- def serverOptions[F[_]: Sync]: Logger => Http4sServerOptions[F] = ServerOptions.create[F, UserError](
+ def serverOptions[F[_]](using Sync[F]): Logger => Http4sServerOptions[F] = ServerOptions.create[F, UserError](
_,
ServerOptions.ErrorHandler[UserError](
() => UserError.ValidationFailed(NonEmptyList.one("Data missing")),
@@ -99,7 +99,7 @@ object ChannelModerationServer {
)
)
- def errorHandler[F[_]: Sync]: Logger => ServerErrorHandler[F, UserError] =
+ def errorHandler[F[_]](using Sync[F]): Logger => ServerErrorHandler[F, UserError] =
ServerErrorHandler.handleCommonErrors[F, UserError] {
case CommonError.InvalidCredentials(_) =>
UserError.BadCredentials("Invalid credentials")
diff --git a/modules/server/src/main/scala/io/branchtalk/users/api/UserBanServer.scala b/modules/server/src/main/scala/io/branchtalk/users/api/UserBanServer.scala
index b0fc0a3b..fde4c16c 100644
--- a/modules/server/src/main/scala/io/branchtalk/users/api/UserBanServer.scala
+++ b/modules/server/src/main/scala/io/branchtalk/users/api/UserBanServer.scala
@@ -3,15 +3,16 @@ package io.branchtalk.users.api
import cats.data.NonEmptyList
import cats.effect.{ Async, Sync }
import com.typesafe.scalalogging.Logger
-import io.branchtalk.api.{ Permission => _, _ }
-import io.branchtalk.auth._
+import io.branchtalk.api.{ Permission => _, * }
+import io.branchtalk.auth.{ *, given }
+import io.branchtalk.shared.infrastructure.*
import io.branchtalk.shared.model.CommonError
-import io.branchtalk.users.api.UserModels._
+import io.branchtalk.users.api.UserModels.*
import io.branchtalk.users.model.{ Ban, User }
import io.branchtalk.users.reads.BanReads
import io.branchtalk.users.writes.BanWrites
-import org.http4s._
-import sttp.tapir.server.http4s._
+import org.http4s.*
+import sttp.tapir.server.http4s.*
import sttp.tapir.server.ServerEndpoint
final class UserBanServer[F[_]: Async](
@@ -20,14 +21,13 @@ final class UserBanServer[F[_]: Async](
banWrites: BanWrites[F]
) {
- implicit private val as: AuthServices[F] = authServices
+ private given AuthServices[F] = authServices
private val logger = Logger(getClass)
- private val serverOptions: Http4sServerOptions[F] = UserBanServer.serverOptions[F].apply(logger)
+ private val serverOptions: Http4sServerOptions[F] = UserBanServer.serverOptions[F](logger)
- implicit private val errorHandler: ServerErrorHandler[F, UserError] =
- UserBanServer.errorHandler[F].apply(logger)
+ private given errorHandler: ServerErrorHandler[F, UserError] = UserBanServer.errorHandler[F](logger)
private val list = UserBanAPIs.list.serverLogic[F, User] { _ =>
for {
@@ -60,7 +60,7 @@ final class UserBanServer[F[_]: Async](
}
object UserBanServer {
- def serverOptions[F[_]: Sync]: Logger => Http4sServerOptions[F] = ServerOptions.create[F, UserError](
+ def serverOptions[F[_]](using Sync[F]): Logger => Http4sServerOptions[F] = ServerOptions.create[F, UserError](
_,
ServerOptions.ErrorHandler[UserError](
() => UserError.ValidationFailed(NonEmptyList.one("Data missing")),
@@ -76,7 +76,7 @@ object UserBanServer {
)
)
- def errorHandler[F[_]: Sync]: Logger => ServerErrorHandler[F, UserError] =
+ def errorHandler[F[_]](using Sync[F]): Logger => ServerErrorHandler[F, UserError] =
ServerErrorHandler.handleCommonErrors[F, UserError] {
case CommonError.InvalidCredentials(_) =>
UserError.BadCredentials("Invalid credentials")
diff --git a/modules/server/src/main/scala/io/branchtalk/users/api/UserModerationServer.scala b/modules/server/src/main/scala/io/branchtalk/users/api/UserModerationServer.scala
index 018046d1..2599bb24 100644
--- a/modules/server/src/main/scala/io/branchtalk/users/api/UserModerationServer.scala
+++ b/modules/server/src/main/scala/io/branchtalk/users/api/UserModerationServer.scala
@@ -3,16 +3,17 @@ package io.branchtalk.users.api
import cats.data.NonEmptyList
import cats.effect.{ Async, Sync }
import com.typesafe.scalalogging.Logger
-import io.branchtalk.api.{ Permission => _, _ }
-import io.branchtalk.auth._
+import io.branchtalk.api.{ Permission => _, * }
+import io.branchtalk.auth.{ *, given }
import io.branchtalk.configs.PaginationConfig
+import io.branchtalk.shared.infrastructure.*
import io.branchtalk.shared.model.{ CommonError, OptionUpdatable, Updatable }
-import io.branchtalk.users.api.UserModels._
+import io.branchtalk.users.api.UserModels.*
import io.branchtalk.users.model.{ Permission, User }
import io.branchtalk.users.reads.UserReads
import io.branchtalk.users.writes.UserWrites
-import org.http4s._
-import sttp.tapir.server.http4s._
+import org.http4s.*
+import sttp.tapir.server.http4s.*
import sttp.tapir.server.ServerEndpoint
final class UserModerationServer[F[_]: Async](
@@ -22,14 +23,13 @@ final class UserModerationServer[F[_]: Async](
paginationConfig: PaginationConfig
) {
- implicit private val as: AuthServices[F] = authServices
+ private given AuthServices[F] = authServices
private val logger = Logger(getClass)
- private val serverOptions: Http4sServerOptions[F] = UserModerationServer.serverOptions[F].apply(logger)
+ private val serverOptions: Http4sServerOptions[F] = UserModerationServer.serverOptions[F](logger)
- implicit private val errorHandler: ServerErrorHandler[F, UserError] =
- UserModerationServer.errorHandler[F].apply(logger)
+ private given errorHandler: ServerErrorHandler[F, UserError] = UserModerationServer.errorHandler[F](logger)
private val paginate = UserModerationAPIs.paginate.serverLogic[F, User] { case (optOffset, optLimit) =>
val sortBy = User.Sorting.NameAlphabetically
@@ -37,7 +37,7 @@ final class UserModerationServer[F[_]: Async](
val limit = paginationConfig.resolveLimit(optLimit)
val filters = List(User.Filter.HasPermission(Permission.ModerateUsers))
for {
- paginated <- userReads.paginate(sortBy, offset.nonNegativeLong, limit.positiveInt, filters)
+ paginated <- userReads.paginate(sortBy, offset, limit, filters)
} yield Pagination.fromPaginated(paginated.map(APIUser.fromDomain), offset, limit)
}
@@ -81,7 +81,7 @@ final class UserModerationServer[F[_]: Async](
object UserModerationServer {
- def serverOptions[F[_]: Sync]: Logger => Http4sServerOptions[F] = ServerOptions.create[F, UserError](
+ def serverOptions[F[_]](using Sync[F]): Logger => Http4sServerOptions[F] = ServerOptions.create[F, UserError](
_,
ServerOptions.ErrorHandler[UserError](
() => UserError.ValidationFailed(NonEmptyList.one("Data missing")),
@@ -97,7 +97,7 @@ object UserModerationServer {
)
)
- def errorHandler[F[_]: Sync]: Logger => ServerErrorHandler[F, UserError] =
+ def errorHandler[F[_]](using Sync[F]): Logger => ServerErrorHandler[F, UserError] =
ServerErrorHandler.handleCommonErrors[F, UserError] {
case CommonError.InvalidCredentials(_) =>
UserError.BadCredentials("Invalid credentials")
diff --git a/modules/server/src/main/scala/io/branchtalk/users/api/UserServer.scala b/modules/server/src/main/scala/io/branchtalk/users/api/UserServer.scala
index 3b11e5b9..c9eb1b7e 100644
--- a/modules/server/src/main/scala/io/branchtalk/users/api/UserServer.scala
+++ b/modules/server/src/main/scala/io/branchtalk/users/api/UserServer.scala
@@ -3,18 +3,19 @@ package io.branchtalk.users.api
import cats.data.NonEmptyList
import cats.effect.{ Async, Sync }
import com.typesafe.scalalogging.Logger
-import io.branchtalk.api._
-import io.branchtalk.auth._
+import io.branchtalk.api.*
+import io.branchtalk.auth.{ *, given }
import io.branchtalk.configs.PaginationConfig
-import io.branchtalk.mappings._
+import io.branchtalk.mappings.*
+import io.branchtalk.shared.infrastructure.*
import io.branchtalk.shared.model.{ CommonError, ID }
-import io.branchtalk.users.api.UserModels._
+import io.branchtalk.users.api.UserModels.*
import io.branchtalk.users.model.{ Password, Session, User }
import io.branchtalk.users.reads.{ SessionReads, UserReads }
import io.branchtalk.users.writes.{ SessionWrites, UserWrites }
-import io.scalaland.chimney.dsl._
-import org.http4s._
-import sttp.tapir.server.http4s._
+import io.scalaland.chimney.dsl.*
+import org.http4s.*
+import sttp.tapir.server.http4s.*
import sttp.tapir.server.ServerEndpoint
final class UserServer[F[_]: Async](
@@ -26,22 +27,22 @@ final class UserServer[F[_]: Async](
paginationConfig: PaginationConfig
) {
- implicit private val as: AuthServices[F] = authServices
+ private given AuthServices[F] = authServices
private val logger = Logger(getClass)
private val sessionExpiresInDays = 7L // make it configurable
- private val serverOptions: Http4sServerOptions[F] = UserServer.serverOptions[F].apply(logger)
+ private val serverOptions: Http4sServerOptions[F] = UserServer.serverOptions[F](logger)
- implicit private val errorHandler: ServerErrorHandler[F, UserError] = UserServer.errorHandler[F].apply(logger)
+ private given errorHandler: ServerErrorHandler[F, UserError] = UserServer.errorHandler[F](logger)
private val paginate = UserAPIs.paginate.serverLogic[F, User] { case (optOffset, optLimit) =>
val sortBy = User.Sorting.NameAlphabetically
val offset = paginationConfig.resolveOffset(optOffset)
val limit = paginationConfig.resolveLimit(optLimit)
for {
- paginated <- userReads.paginate(sortBy, offset.nonNegativeLong, limit.positiveInt)
+ paginated <- userReads.paginate(sortBy, offset, limit)
} yield Pagination.fromPaginated(paginated.map(APIUser.fromDomain), offset, limit)
}
@@ -50,7 +51,7 @@ final class UserServer[F[_]: Async](
val offset = paginationConfig.resolveOffset(optOffset)
val limit = paginationConfig.resolveLimit(optLimit)
for {
- paginated <- userReads.paginate(sortBy, offset.nonNegativeLong, limit.positiveInt)
+ paginated <- userReads.paginate(sortBy, offset, limit)
} yield Pagination.fromPaginated(paginated.map(APIUser.fromDomain), offset, limit)
}
@@ -59,7 +60,7 @@ final class UserServer[F[_]: Async](
val offset = paginationConfig.resolveOffset(optOffset)
val limit = paginationConfig.resolveLimit(optLimit)
for {
- paginated <- sessionReads.paginate(user.id, sortBy, offset.nonNegativeLong, limit.positiveInt)
+ paginated <- sessionReads.paginate(user.id, sortBy, offset, limit)
} yield Pagination.fromPaginated(paginated.map(APISession.fromDomain), offset, limit)
}
@@ -69,7 +70,7 @@ final class UserServer[F[_]: Async](
(user, session) <- userWrites.createUser(
signup.into[User.Create].withFieldConst(_.password, Password.create(signup.password)).transform
)
- } yield SignUpResponse(user.id, session.id)
+ } yield SignUpResponse(user.unwrap, session.unwrap)
}
}
@@ -153,7 +154,7 @@ final class UserServer[F[_]: Async](
}
object UserServer {
- def serverOptions[F[_]: Sync]: Logger => Http4sServerOptions[F] = ServerOptions.create[F, UserError](
+ def serverOptions[F[_]](using Sync[F]): Logger => Http4sServerOptions[F] = ServerOptions.create[F, UserError](
_,
ServerOptions.ErrorHandler[UserError](
() => UserError.ValidationFailed(NonEmptyList.one("Data missing")),
@@ -169,7 +170,7 @@ object UserServer {
)
)
- def errorHandler[F[_]: Sync]: Logger => ServerErrorHandler[F, UserError] =
+ def errorHandler[F[_]](using Sync[F]): Logger => ServerErrorHandler[F, UserError] =
ServerErrorHandler.handleCommonErrors[F, UserError] {
case CommonError.InvalidCredentials(_) =>
UserError.BadCredentials("Invalid credentials")
diff --git a/modules/server/src/it/scala/io/branchtalk/TestDependencies.scala b/modules/server/src/test/scala/io/branchtalk/TestDependencies.scala
similarity index 74%
rename from modules/server/src/it/scala/io/branchtalk/TestDependencies.scala
rename to modules/server/src/test/scala/io/branchtalk/TestDependencies.scala
index 25ddd923..59b1c726 100644
--- a/modules/server/src/it/scala/io/branchtalk/TestDependencies.scala
+++ b/modules/server/src/test/scala/io/branchtalk/TestDependencies.scala
@@ -2,10 +2,9 @@ package io.branchtalk
import cats.effect.std.Dispatcher
import cats.effect.{ Async, Resource }
-import com.softwaremill.macwire.wire
import io.branchtalk.discussions.{ DiscussionsModule, DiscussionsReads, DiscussionsWrites, TestDiscussionsConfig }
import io.branchtalk.logging.MDC
-import io.branchtalk.shared.model.UUIDGenerator
+import io.branchtalk.shared.model.UUID
import io.branchtalk.users.{ TestUsersConfig, UsersModule, UsersReads, UsersWrites }
import io.prometheus.client.CollectorRegistry
@@ -19,17 +18,14 @@ final case class TestDependencies[F[_]](
)
object TestDependencies {
- @nowarn("cat=unused") // macwire
- def resources[F[_]: Async: MDC](registry: CollectorRegistry)(implicit
- uuidGenerator: UUIDGenerator
- ): Resource[F, TestDependencies[F]] =
+ def resources[F[_]: Async: MDC](registry: CollectorRegistry)(using UUID.Generator): Resource[F, TestDependencies[F]] =
for {
- implicit0(dispatcher: Dispatcher[F]) <- Dispatcher[F]
+ given Dispatcher[F] <- Dispatcher.parallel[F]
usersConfig <- TestUsersConfig.loadDomainConfig[F]
usersReads <- UsersModule.reads[F](usersConfig, registry)
usersWrites <- UsersModule.writes[F](usersConfig, registry)
discussionsConfig <- TestDiscussionsConfig.loadDomainConfig[F]
discussionsReads <- DiscussionsModule.reads[F](discussionsConfig, registry)
discussionsWrites <- DiscussionsModule.writes[F](discussionsConfig, registry)
- } yield wire[TestDependencies[F]]
+ } yield TestDependencies[F](usersReads, usersWrites, discussionsReads, discussionsWrites)
}
diff --git a/modules/server/src/it/scala/io/branchtalk/api/ServerIOTest.scala b/modules/server/src/test/scala/io/branchtalk/api/ServerIOTest.scala
similarity index 72%
rename from modules/server/src/it/scala/io/branchtalk/api/ServerIOTest.scala
rename to modules/server/src/test/scala/io/branchtalk/api/ServerIOTest.scala
index ec75fdc0..2d0d3143 100644
--- a/modules/server/src/it/scala/io/branchtalk/api/ServerIOTest.scala
+++ b/modules/server/src/test/scala/io/branchtalk/api/ServerIOTest.scala
@@ -3,15 +3,16 @@ package io.branchtalk.api
import cats.effect.{ IO, Resource }
import io.branchtalk.discussions.DiscussionsIOTest
import io.branchtalk.users.{ UsersIOTest, UsersModule }
+import io.branchtalk.shared.infrastructure.*
import org.http4s.server.Server
import org.specs2.matcher.{ OptionLikeCheckedMatcher, OptionLikeMatcher, ValueCheck }
import sttp.client3.{ Response, SttpBackend }
import sttp.client3.asynchttpclient.cats.AsyncHttpClientCatsBackend
import sttp.model.Uri
-import sttp.tapir._
-import sttp.tapir.client.sttp._
+import sttp.tapir.*
+import sttp.tapir.client.sttp.*
-trait ServerIOTest extends UsersIOTest with DiscussionsIOTest {
+trait ServerIOTest extends UsersIOTest, DiscussionsIOTest {
// populated by resources
protected var server: Server = _
@@ -53,9 +54,9 @@ trait ServerIOTest extends UsersIOTest with DiscussionsIOTest {
override protected def testResource: Resource[IO, Unit] = super.testResource >> serverResource
- implicit class ServerTestOps[I, E, O](private val endpoint: Endpoint[Unit, I, E, O, Any]) {
+ extension [I, E, O](endpoint: Endpoint[Unit, I, E, O, Any]) {
- val toTestCall: I => IO[Response[DecodeResult[Either[E, O]]]] = (input: I) =>
+ def toTestCall: I => IO[Response[DecodeResult[Either[E, O]]]] = (input: I) =>
SttpClientInterpreter(SttpClientOptions.default)
.toRequest(
endpoint,
@@ -66,9 +67,9 @@ trait ServerIOTest extends UsersIOTest with DiscussionsIOTest {
.send(client)
}
- implicit class AuthServerTestOps[A, I, E, O](private val authEndpoint: AuthedEndpoint[A, I, E, O, Any]) {
+ extension [A, I, E, O](authEndpoint: AuthedEndpoint[A, I, E, O, Any]) {
- val toTestCall: (A, I) => IO[Response[DecodeResult[Either[E, O]]]] = (auth: A, input: I) =>
+ def toTestCall: (A, I) => IO[Response[DecodeResult[Either[E, O]]]] = (auth: A, input: I) =>
SttpClientInterpreter(SttpClientOptions.default)
.toSecureRequest(
authEndpoint.endpoint,
@@ -80,9 +81,9 @@ trait ServerIOTest extends UsersIOTest with DiscussionsIOTest {
.send(client)
}
- implicit class AuthOnlyServerTestOps[A, E, O](private val authEndpoint: AuthedEndpoint[A, Unit, E, O, Any]) {
+ extension [A, E, O](authEndpoint: AuthedEndpoint[A, Unit, E, O, Any]) {
- val toTestCall: A => IO[Response[DecodeResult[Either[E, O]]]] = (auth: A) =>
+ def toTestCall: A => IO[Response[DecodeResult[Either[E, O]]]] = (auth: A) =>
SttpClientInterpreter(SttpClientOptions.default)
.toSecureRequest(
authEndpoint.endpoint,
@@ -94,19 +95,16 @@ trait ServerIOTest extends UsersIOTest with DiscussionsIOTest {
.send(client)
}
- import ServerIOTest._
+ import ServerIOTest.*
+ export ServerIOTest.toValidOpt
- implicit def toDecoderResultOps[A](result: DecodeResult[A]): DecodeResultOps[A] = new DecodeResultOps[A](result)
-
- import org.specs2.control.ImplicitParameters._
- def beValid[T](t: ValueCheck[T]): ValidResultCheckedMatcher[T] = ValidResultCheckedMatcher(t)
- def beValid[T](implicit p: ImplicitParam = implicitParameter): ValidResultMatcher[T] = use(p)(ValidResultMatcher[T]())
+ def beValid[T](t: ValueCheck[T]): ValidResultCheckedMatcher[T] = ValidResultCheckedMatcher(t)
+ def beValid[T](using DummyImplicit): ValidResultMatcher[T] = ValidResultMatcher[T]()
}
object ServerIOTest {
- implicit class DecodeResultOps[A](private val result: DecodeResult[A]) extends AnyVal {
-
+ extension [A](result: DecodeResult[A]) {
def toValidOpt: Option[A] = result match {
case DecodeResult.Value(t) => t.some
case _ => none[A]
@@ -114,8 +112,8 @@ object ServerIOTest {
}
final case class ValidResultMatcher[T]()
- extends OptionLikeMatcher[DecodeResult, T, T]("DecodeResult.Value", (_: DecodeResult[T]).toValidOpt)
+ extends OptionLikeMatcher[DecodeResult[T], T]("DecodeResult.Value", (_: DecodeResult[T]).toValidOpt)
final case class ValidResultCheckedMatcher[T](check: ValueCheck[T])
- extends OptionLikeCheckedMatcher[DecodeResult, T, T]("DecodeResult.Value", (_: DecodeResult[T]).toValidOpt, check)
+ extends OptionLikeCheckedMatcher[DecodeResult[T], T]("DecodeResult.Value", (_: DecodeResult[T]).toValidOpt, check)
}
diff --git a/modules/server/src/it/scala/io/branchtalk/api/TestApiConfigs.scala b/modules/server/src/test/scala/io/branchtalk/api/TestApiConfigs.scala
similarity index 72%
rename from modules/server/src/it/scala/io/branchtalk/api/TestApiConfigs.scala
rename to modules/server/src/test/scala/io/branchtalk/api/TestApiConfigs.scala
index cdcf9920..2d977c60 100644
--- a/modules/server/src/it/scala/io/branchtalk/api/TestApiConfigs.scala
+++ b/modules/server/src/test/scala/io/branchtalk/api/TestApiConfigs.scala
@@ -1,11 +1,14 @@
package io.branchtalk.api
+import java.net.URI
import cats.effect.{ Async, Resource, Sync }
import io.branchtalk.configs.{ APIConfig, APIContact, APIHttp, APIInfo, APILicense, AppArguments }
-import io.branchtalk.shared.model.UUIDGenerator
+import io.branchtalk.discussions.model.Post
+import io.branchtalk.shared.model.UUID
+import io.branchtalk.users.model.User
import scala.collection.mutable
-import scala.concurrent.duration._
+import scala.concurrent.duration.*
object TestApiConfigs {
@@ -30,7 +33,7 @@ object TestApiConfigs {
private def portResource[F[_]: Async]: Resource[F, Int] = Resource.make(acquirePort[F])(releasePort[F](_))
- def asResource[F[_]: Async](implicit uuidGenerator: UUIDGenerator): Resource[F, (AppArguments, APIConfig)] =
+ def asResource[F[_]: Async](using uuidGenerator: UUID.Generator): Resource[F, (AppArguments, APIConfig)] =
(Resource.eval(uuidGenerator.create[F]), portResource[F]).mapN { (defaultChannelID, port) =>
val host = "localhost"
val app = AppArguments(
@@ -45,9 +48,13 @@ object TestApiConfigs {
title = "test",
version = "test",
description = "test",
- termsOfService = "http://branchtalk.io",
- contact = APIContact(name = "test", email = "test@brachtalk.io", url = "http://branchtalk.io"),
- license = APILicense(name = "test", url = "http://branchtalk.io")
+ termsOfService = Post.URL.unsafeMake(URI.create("http://branchtalk.io")),
+ contact = APIContact(
+ name = "test",
+ email = User.Email.unsafeMake("test@brachtalk.io"),
+ url = Post.URL.unsafeMake(URI.create("http://branchtalk.io"))
+ ),
+ license = APILicense(name = "test", url = Post.URL.unsafeMake(URI.create("http://branchtalk.io")))
),
http = APIHttp(
logHeaders = true,
diff --git a/modules/server/src/it/scala/io/branchtalk/discussions/api/ChannelServerPaginationSpec.scala b/modules/server/src/test/scala/io/branchtalk/discussions/api/ChannelServerPaginationSpec.scala
similarity index 64%
rename from modules/server/src/it/scala/io/branchtalk/discussions/api/ChannelServerPaginationSpec.scala
rename to modules/server/src/test/scala/io/branchtalk/discussions/api/ChannelServerPaginationSpec.scala
index 3ea8dd94..74f76914 100644
--- a/modules/server/src/it/scala/io/branchtalk/discussions/api/ChannelServerPaginationSpec.scala
+++ b/modules/server/src/test/scala/io/branchtalk/discussions/api/ChannelServerPaginationSpec.scala
@@ -2,22 +2,19 @@ package io.branchtalk.discussions.api
import io.branchtalk.api.{ Permission => _, RequiredPermissions => _, _ }
import io.branchtalk.discussions.DiscussionsFixtures
-import io.branchtalk.discussions.api.ChannelModels._
-import io.branchtalk.shared.model._
+import io.branchtalk.discussions.api.ChannelModels.*
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.infrastructure.*
import io.branchtalk.users.UsersFixtures
import org.specs2.mutable.Specification
import sttp.model.StatusCode
-final class ChannelServerPaginationSpec
- extends Specification
- with ServerIOTest
- with UsersFixtures
- with DiscussionsFixtures {
+final class ChannelServerPaginationSpec extends Specification, ServerIOTest, UsersFixtures, DiscussionsFixtures {
// Channel pagination tests cannot be run in parallel to other Channel tests (no parent to filter other tests)
sequential
- implicit protected lazy val uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
+ protected given uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
"ChannelServer-provided pagination endpoints" should {
@@ -27,26 +24,24 @@ final class ChannelServerPaginationSpec
for {
// given
channelIDs <- (0 until 10).toList.traverse(_ =>
- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.id)
+ channelCreate.flatMap(discussionsWrites.channelWrites.createChannel).map(_.unwrap)
)
channels <- channelIDs.traverse(discussionsReads.channelReads.requireById(_)).eventually()
// when
- response1 <- ChannelAPIs.paginate.toTestCall.untupled(None, None, PaginationLimit(5).some)
+ response1 <- ChannelAPIs.paginate.toTestCall.untupled(None, None, Pagination.Limit(5).some)
response2 <- ChannelAPIs.paginate.toTestCall.untupled(None,
- PaginationOffset(5L).some,
- PaginationLimit(5).some
+ Pagination.Offset(5L).some,
+ Pagination.Limit(5).some
)
} yield {
// then
- response1.code must_=== StatusCode.Ok
- response1.body must beValid(beRight(anInstanceOf[Pagination[APIChannel]]))
- response2.code must_=== StatusCode.Ok
- response2.body must beValid(beRight(anInstanceOf[Pagination[APIChannel]]))
+ response1.code === StatusCode.Ok
+ response1.body must beValid(beRight(beAnInstanceOf[Pagination[APIChannel]]))
+ response2.code === StatusCode.Ok
+ response2.body must beValid(beRight(beAnInstanceOf[Pagination[APIChannel]]))
(response1.body.toValidOpt.flatMap(_.toOption), response2.body.toValidOpt.flatMap(_.toOption))
.mapN { (pagination1, pagination2) =>
- (pagination1.entities.toSet ++ pagination2.entities.toSet) must_=== channels
- .map(APIChannel.fromDomain)
- .toSet
+ (pagination1.entities.toSet ++ pagination2.entities.toSet) === channels.map(APIChannel.fromDomain).toSet
}
.getOrElse(pass)
}
diff --git a/modules/server/src/it/scala/io/branchtalk/discussions/api/ChannelServerSpec.scala b/modules/server/src/test/scala/io/branchtalk/discussions/api/ChannelServerSpec.scala
similarity index 78%
rename from modules/server/src/it/scala/io/branchtalk/discussions/api/ChannelServerSpec.scala
rename to modules/server/src/test/scala/io/branchtalk/discussions/api/ChannelServerSpec.scala
index 04c2e0ea..5f49a8b9 100644
--- a/modules/server/src/it/scala/io/branchtalk/discussions/api/ChannelServerSpec.scala
+++ b/modules/server/src/test/scala/io/branchtalk/discussions/api/ChannelServerSpec.scala
@@ -1,22 +1,23 @@
package io.branchtalk.discussions.api
import cats.effect.IO
-import io.branchtalk.api.{ Permission => _, RequiredPermissions => _, _ }
+import com.softwaremill.quicklens.*
+import io.branchtalk.api.{ Permission => _, RequiredPermissions => _, * }
import io.branchtalk.discussions.DiscussionsFixtures
-import io.branchtalk.discussions.api.ChannelModels._
+import io.branchtalk.discussions.api.ChannelModels.*
import io.branchtalk.discussions.model.Channel
-import io.branchtalk.mappings._
-import io.branchtalk.shared.model._
+import io.branchtalk.mappings.*
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.infrastructure.*
import io.branchtalk.users.UsersFixtures
import io.branchtalk.users.model.{ Permission, RequiredPermissions }
-import io.scalaland.chimney.dsl._
-import monocle.macros.syntax.lens._
+import io.scalaland.chimney.dsl.*
import org.specs2.mutable.Specification
import sttp.model.StatusCode
-final class ChannelServerSpec extends Specification with ServerIOTest with UsersFixtures with DiscussionsFixtures {
+final class ChannelServerSpec extends Specification, ServerIOTest, UsersFixtures, DiscussionsFixtures {
- implicit protected lazy val uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
+ protected given uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
"ChannelServer-provided endpoints" should {
@@ -41,8 +42,8 @@ final class ChannelServerSpec extends Specification with ServerIOTest with Users
// TODO: check that this creates a new channel eventually!
} yield {
// then
- response.code must_=== StatusCode.Ok
- response.body must beValid(beRight(anInstanceOf[CreateChannelResponse]))
+ response.code === StatusCode.Ok
+ response.body must beValid(beRight(beAnInstanceOf[CreateChannelResponse]))
}
}
}
@@ -66,8 +67,8 @@ final class ChannelServerSpec extends Specification with ServerIOTest with Users
)
} yield {
// then
- response.code must_=== StatusCode.Ok
- response.body must beValid(beRight(be_===(APIChannel.fromDomain(channel))))
+ response.code === StatusCode.Ok
+ response.body must beValid(beRight(be_==(APIChannel.fromDomain(channel))))
}
}
}
@@ -83,7 +84,7 @@ final class ChannelServerSpec extends Specification with ServerIOTest with Users
_ <- usersReads.userReads.requireById(userID).eventually()
_ <- usersReads.sessionReads.requireById(sessionID).eventually()
CreationScheduled(channelID) <- channelCreate
- .map(_.focus(_.authorID).replace(userIDUsers2Discussions.get(userID))) // make User Channels' owner
+ .map(_.modify(_.authorID).setTo(userIDUsers2Discussions.get(userID))) // make User Channels' owner
.flatMap(discussionsWrites.channelWrites.createChannel)
channel <- discussionsReads.channelReads.requireById(channelID).eventually()
_ <- usersReads.userReads
@@ -94,9 +95,9 @@ final class ChannelServerSpec extends Specification with ServerIOTest with Users
)
)
.eventually()
- newUrlName <- Channel.UrlName.parse[IO]("new-name")
- newName <- Channel.Name.parse[IO]("new name")
- newDescription <- Channel.Description.parse[IO]("lorem ipsum")
+ newUrlName <- ParseNewtype[IO].parse[Channel.UrlName]("new-name")
+ newName <- ParseNewtype[IO].parse[Channel.Name]("new name")
+ newDescription <- ParseNewtype[IO].parse[Channel.Description]("lorem ipsum")
// when
response <- ChannelAPIs.update.toTestCall.untupled(
Authentication.Session(sessionID = sessionIDApi2Users.reverseGet(sessionID)),
@@ -113,17 +114,17 @@ final class ChannelServerSpec extends Specification with ServerIOTest with Users
.eventually()
} yield {
// then
- response.code must_=== StatusCode.Ok
- response.body must beValid(beRight(be_===(UpdateChannelResponse(channelID))))
- updatedChannel must_=== channel
- .focus(_.data.urlName)
- .replace(newUrlName)
- .focus(_.data.name)
- .replace(newName)
- .focus(_.data.description)
- .replace(newDescription.some)
- .focus(_.data.lastModifiedAt)
- .replace(updatedChannel.data.lastModifiedAt)
+ response.code === StatusCode.Ok
+ response.body must beValid(beRight(be_==(UpdateChannelResponse(channelID))))
+ updatedChannel === channel
+ .modify(_.data.urlName)
+ .setTo(newUrlName)
+ .modify(_.data.name)
+ .setTo(newName)
+ .modify(_.data.description)
+ .setTo(newDescription.some)
+ .modify(_.data.lastModifiedAt)
+ .setTo(updatedChannel.data.lastModifiedAt)
}
}
}
@@ -139,7 +140,7 @@ final class ChannelServerSpec extends Specification with ServerIOTest with Users
_ <- usersReads.userReads.requireById(userID).eventually()
_ <- usersReads.sessionReads.requireById(sessionID).eventually()
CreationScheduled(channelID) <- channelCreate
- .map(_.focus(_.authorID).replace(userIDUsers2Discussions.get(userID))) // make User Channels' owner
+ .map(_.modify(_.authorID).setTo(userIDUsers2Discussions.get(userID))) // make User Channels' owner
.flatMap(discussionsWrites.channelWrites.createChannel)
_ <- discussionsReads.channelReads.requireById(channelID).eventually()
_ <- usersReads.userReads
@@ -161,8 +162,8 @@ final class ChannelServerSpec extends Specification with ServerIOTest with Users
.eventually()
} yield {
// then
- response.code must_=== StatusCode.Ok
- response.body must beValid(beRight(be_===(DeleteChannelResponse(channelID))))
+ response.code === StatusCode.Ok
+ response.body must beValid(beRight(be_==(DeleteChannelResponse(channelID))))
}
}
}
@@ -178,7 +179,7 @@ final class ChannelServerSpec extends Specification with ServerIOTest with Users
_ <- usersReads.userReads.requireById(userID).eventually()
_ <- usersReads.sessionReads.requireById(sessionID).eventually()
CreationScheduled(channelID) <- channelCreate
- .map(_.focus(_.authorID).replace(userIDUsers2Discussions.get(userID))) // make User Channels' owner
+ .map(_.modify(_.authorID).setTo(userIDUsers2Discussions.get(userID))) // make User Channels' owner
.flatMap(discussionsWrites.channelWrites.createChannel)
_ <- discussionsReads.channelReads.requireById(channelID).eventually()
_ <- usersReads.userReads
@@ -204,8 +205,8 @@ final class ChannelServerSpec extends Specification with ServerIOTest with Users
.eventually()
} yield {
// then
- response.code must_=== StatusCode.Ok
- response.body must beValid(beRight(be_===(RestoreChannelResponse(channelID))))
+ response.code === StatusCode.Ok
+ response.body must beValid(beRight(be_==(RestoreChannelResponse(channelID))))
}
}
}
diff --git a/modules/server/src/it/scala/io/branchtalk/discussions/api/CommentServerSpec.scala b/modules/server/src/test/scala/io/branchtalk/discussions/api/CommentServerSpec.scala
similarity index 85%
rename from modules/server/src/it/scala/io/branchtalk/discussions/api/CommentServerSpec.scala
rename to modules/server/src/test/scala/io/branchtalk/discussions/api/CommentServerSpec.scala
index 477fece1..3b521f24 100644
--- a/modules/server/src/it/scala/io/branchtalk/discussions/api/CommentServerSpec.scala
+++ b/modules/server/src/test/scala/io/branchtalk/discussions/api/CommentServerSpec.scala
@@ -1,20 +1,21 @@
package io.branchtalk.discussions.api
-import io.branchtalk.api.{ Authentication, Pagination, PaginationLimit, PaginationOffset, ServerIOTest }
+import com.softwaremill.quicklens.*
+import io.branchtalk.api.{ Authentication, Pagination, ServerIOTest }
import io.branchtalk.discussions.DiscussionsFixtures
-import io.branchtalk.discussions.api.CommentModels._
+import io.branchtalk.discussions.api.CommentModels.*
import io.branchtalk.discussions.model.Comment
-import io.branchtalk.mappings._
-import io.branchtalk.shared.model._
+import io.branchtalk.mappings.*
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.infrastructure.*
import io.branchtalk.users.UsersFixtures
-import io.scalaland.chimney.dsl._
-import monocle.macros.syntax.lens._
+import io.scalaland.chimney.dsl.*
import org.specs2.mutable.Specification
import sttp.model.StatusCode
-final class CommentServerSpec extends Specification with ServerIOTest with UsersFixtures with DiscussionsFixtures {
+final class CommentServerSpec extends Specification, ServerIOTest, UsersFixtures, DiscussionsFixtures {
- implicit protected lazy val uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
+ protected given uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
"CommentServer-provided endpoints" should {
@@ -28,7 +29,7 @@ final class CommentServerSpec extends Specification with ServerIOTest with Users
CreationScheduled(postID) <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost)
_ <- discussionsReads.postReads.requireById(postID).eventually()
commentIDs <- (0 until 10).toList.traverse(_ =>
- commentCreate(postID).flatMap(discussionsWrites.commentWrites.createComment).map(_.id)
+ commentCreate(postID).flatMap(discussionsWrites.commentWrites.createComment).map(_.unwrap)
)
comments <- commentIDs.traverse(discussionsReads.commentReads.requireById(_)).eventually()
// when
@@ -36,27 +37,25 @@ final class CommentServerSpec extends Specification with ServerIOTest with Users
channelID,
postID,
None,
- PaginationLimit(5).some,
+ Pagination.Limit(5).some,
None
)
response2 <- CommentAPIs.newest.toTestCall.untupled(None,
channelID,
postID,
- PaginationOffset(5L).some,
- PaginationLimit(5).some,
+ Pagination.Offset(5L).some,
+ Pagination.Limit(5).some,
None
)
} yield {
// then
- response1.code must_=== StatusCode.Ok
- response1.body must beValid(beRight(anInstanceOf[Pagination[APIComment]]))
- response2.code must_=== StatusCode.Ok
- response2.body must beValid(beRight(anInstanceOf[Pagination[APIComment]]))
+ response1.code === StatusCode.Ok
+ response1.body must beValid(beRight(beAnInstanceOf[Pagination[APIComment]]))
+ response2.code === StatusCode.Ok
+ response2.body must beValid(beRight(beAnInstanceOf[Pagination[APIComment]]))
(response1.body.toValidOpt.flatMap(_.toOption), response2.body.toValidOpt.flatMap(_.toOption))
.mapN { (pagination1, pagination2) =>
- (pagination1.entities.toSet ++ pagination2.entities.toSet) must_=== comments
- .map(APIComment.fromDomain)
- .toSet
+ (pagination1.entities.toSet ++ pagination2.entities.toSet) === comments.map(APIComment.fromDomain).toSet
}
.getOrElse(pass)
}
@@ -73,19 +72,19 @@ final class CommentServerSpec extends Specification with ServerIOTest with Users
CreationScheduled(postID) <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost)
_ <- discussionsReads.postReads.requireById(postID).eventually()
commentIDs <- (0 until 10).toList.traverse(_ =>
- commentCreate(postID).flatMap(discussionsWrites.commentWrites.createComment).map(_.id)
+ commentCreate(postID).flatMap(discussionsWrites.commentWrites.createComment).map(_.unwrap)
)
comments <- commentIDs.traverse(discussionsReads.commentReads.requireById(_)).eventually()
// when
response <- CommentAPIs.hottest.toTestCall.untupled(None, channelID, postID, None)
} yield {
// then
- response.code must_=== StatusCode.Ok
- response.body must beValid(beRight(anInstanceOf[Pagination[APIComment]]))
+ response.code === StatusCode.Ok
+ response.body must beValid(beRight(beAnInstanceOf[Pagination[APIComment]]))
response.body.toValidOpt
.flatMap(_.toOption)
.map { pagination =>
- pagination.entities.toSet must_=== comments.map(APIComment.fromDomain).toSet
+ pagination.entities.toSet === comments.map(APIComment.fromDomain).toSet
}
.getOrElse(pass)
}
@@ -102,18 +101,18 @@ final class CommentServerSpec extends Specification with ServerIOTest with Users
CreationScheduled(postID) <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost)
_ <- discussionsReads.postReads.requireById(postID).eventually()
commentIDs <- (0 until 10).toList.traverse(_ =>
- commentCreate(postID).flatMap(discussionsWrites.commentWrites.createComment).map(_.id)
+ commentCreate(postID).flatMap(discussionsWrites.commentWrites.createComment).map(_.unwrap)
)
comments <- commentIDs.traverse(discussionsReads.commentReads.requireById(_)).eventually()
// when
response <- CommentAPIs.controversial.toTestCall.untupled(None, channelID, postID, None)
} yield {
// then
- response.code must_=== StatusCode.Ok
- response.body must beValid(beRight(anInstanceOf[Pagination[APIComment]]))
+ response.code === StatusCode.Ok
+ response.body must beValid(beRight(beAnInstanceOf[Pagination[APIComment]]))
response.body.toValidOpt
.flatMap(_.toOption)
- .map(_.entities.toSet must_=== comments.map(APIComment.fromDomain).toSet)
+ .map(_.entities.toSet === comments.map(APIComment.fromDomain).toSet)
.getOrElse(pass)
}
}
@@ -144,8 +143,8 @@ final class CommentServerSpec extends Specification with ServerIOTest with Users
// TODO: check that this creates a new comment eventually!
} yield {
// then
- response.code must_=== StatusCode.Ok
- response.body must beValid(beRight(anInstanceOf[CreateCommentResponse]))
+ response.code === StatusCode.Ok
+ response.body must beValid(beRight(beAnInstanceOf[CreateCommentResponse]))
}
}
}
@@ -177,8 +176,8 @@ final class CommentServerSpec extends Specification with ServerIOTest with Users
)
} yield {
// then
- response.code must_=== StatusCode.Ok
- response.body must beValid(beRight(be_===(APIComment.fromDomain(comment))))
+ response.code === StatusCode.Ok
+ response.body must beValid(beRight(be_==(APIComment.fromDomain(comment))))
}
}
}
@@ -198,7 +197,7 @@ final class CommentServerSpec extends Specification with ServerIOTest with Users
CreationScheduled(postID) <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost)
_ <- discussionsReads.postReads.requireById(postID).eventually()
CreationScheduled(commentID) <- commentCreate(postID)
- .map(_.focus(_.authorID).replace(userIDUsers2Discussions.get(userID))) // to own the Comment
+ .map(_.modify(_.authorID).setTo(userIDUsers2Discussions.get(userID))) // to own the Comment
.flatMap(discussionsWrites.commentWrites.createComment)
comment <- discussionsReads.commentReads.requireById(commentID).eventually()
newContent = Comment.Content("lorem ipsum")
@@ -218,13 +217,13 @@ final class CommentServerSpec extends Specification with ServerIOTest with Users
.eventually()
} yield {
// then
- response.code must_=== StatusCode.Ok
- response.body must beValid(beRight(be_===(UpdateCommentResponse(commentID))))
- updatedComment must_=== comment
- .focus(_.data.content)
- .replace(newContent)
- .focus(_.data.lastModifiedAt)
- .replace(updatedComment.data.lastModifiedAt)
+ response.code === StatusCode.Ok
+ response.body must beValid(beRight(be_==(UpdateCommentResponse(commentID))))
+ updatedComment === comment
+ .modify(_.data.content)
+ .setTo(newContent)
+ .modify(_.data.lastModifiedAt)
+ .setTo(updatedComment.data.lastModifiedAt)
}
}
}
@@ -244,7 +243,7 @@ final class CommentServerSpec extends Specification with ServerIOTest with Users
CreationScheduled(postID) <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost)
_ <- discussionsReads.postReads.requireById(postID).eventually()
CreationScheduled(commentID) <- commentCreate(postID)
- .map(_.focus(_.authorID).replace(userIDUsers2Discussions.get(userID))) // to own the Comment
+ .map(_.modify(_.authorID).setTo(userIDUsers2Discussions.get(userID))) // to own the Comment
.flatMap(discussionsWrites.commentWrites.createComment)
_ <- discussionsReads.commentReads.requireById(commentID).eventually()
// when
@@ -260,8 +259,8 @@ final class CommentServerSpec extends Specification with ServerIOTest with Users
.eventually()
} yield {
// then
- response.code must_=== StatusCode.Ok
- response.body must beValid(beRight(be_===(DeleteCommentResponse(commentID))))
+ response.code === StatusCode.Ok
+ response.body must beValid(beRight(be_==(DeleteCommentResponse(commentID))))
}
}
}
@@ -281,7 +280,7 @@ final class CommentServerSpec extends Specification with ServerIOTest with Users
CreationScheduled(postID) <- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost)
_ <- discussionsReads.postReads.requireById(postID).eventually()
CreationScheduled(commentID) <- commentCreate(postID)
- .map(_.focus(_.authorID).replace(userIDUsers2Discussions.get(userID))) // to own the Comment
+ .map(_.modify(_.authorID).setTo(userIDUsers2Discussions.get(userID))) // to own the Comment
.flatMap(discussionsWrites.commentWrites.createComment)
_ <- discussionsReads.commentReads.requireById(commentID).eventually()
_ <- discussionsWrites.commentWrites.deleteComment(
@@ -301,8 +300,8 @@ final class CommentServerSpec extends Specification with ServerIOTest with Users
.eventually()
} yield {
// then
- response.code must_=== StatusCode.Ok
- response.body must beValid(beRight(be_===(RestoreCommentResponse(commentID))))
+ response.code === StatusCode.Ok
+ response.body must beValid(beRight(be_==(RestoreCommentResponse(commentID))))
}
}
}
@@ -332,11 +331,11 @@ final class CommentServerSpec extends Specification with ServerIOTest with Users
)
_ <- discussionsReads.commentReads
.requireById(commentID)
- .assert("Upvoted entity should have changed score")(_.data.totalScore.toInt =!= 0)
+ .assert("Upvoted entity should have changed score")(_.data.totalScore.unwrap =!= 0)
.eventually()
} yield
// then
- response.code must_=== StatusCode.Ok
+ response.code === StatusCode.Ok
}
}
@@ -365,11 +364,11 @@ final class CommentServerSpec extends Specification with ServerIOTest with Users
)
_ <- discussionsReads.commentReads
.requireById(commentID)
- .assert("Downvoted entity should have changed score")(_.data.totalScore.toInt =!= 0)
+ .assert("Downvoted entity should have changed score")(_.data.totalScore.unwrap =!= 0)
.eventually()
} yield
// then
- response.code must_=== StatusCode.Ok
+ response.code === StatusCode.Ok
}
}
@@ -394,7 +393,7 @@ final class CommentServerSpec extends Specification with ServerIOTest with Users
)
_ <- discussionsReads.commentReads
.requireById(commentID)
- .assert("Upvoted entity should have changed score")(_.data.totalScore.toInt =!= 0)
+ .assert("Upvoted entity should have changed score")(_.data.totalScore.unwrap =!= 0)
.eventually()
// when
response <- CommentAPIs.revokeVote.toTestCall.untupled(
@@ -405,11 +404,11 @@ final class CommentServerSpec extends Specification with ServerIOTest with Users
)
_ <- discussionsReads.commentReads
.requireById(commentID)
- .assert("Revoked-vote entity should have changed score")(_.data.totalScore.toInt === 0)
+ .assert("Revoked-vote entity should have changed score")(_.data.totalScore.unwrap eqv 0)
.eventually()
} yield
// then
- response.code must_=== StatusCode.Ok
+ response.code === StatusCode.Ok
}
}
}
diff --git a/modules/server/src/it/scala/io/branchtalk/discussions/api/PostServerSpec.scala b/modules/server/src/test/scala/io/branchtalk/discussions/api/PostServerSpec.scala
similarity index 82%
rename from modules/server/src/it/scala/io/branchtalk/discussions/api/PostServerSpec.scala
rename to modules/server/src/test/scala/io/branchtalk/discussions/api/PostServerSpec.scala
index 1d8acef8..8d77e804 100644
--- a/modules/server/src/it/scala/io/branchtalk/discussions/api/PostServerSpec.scala
+++ b/modules/server/src/test/scala/io/branchtalk/discussions/api/PostServerSpec.scala
@@ -1,21 +1,22 @@
package io.branchtalk.discussions.api
import cats.effect.IO
-import io.branchtalk.api.{ Authentication, Pagination, PaginationLimit, PaginationOffset, ServerIOTest }
+import com.softwaremill.quicklens.*
+import io.branchtalk.api.{ Authentication, Pagination, ServerIOTest }
import io.branchtalk.discussions.DiscussionsFixtures
-import io.branchtalk.discussions.api.PostModels._
+import io.branchtalk.discussions.api.PostModels.*
import io.branchtalk.discussions.model.Post
-import io.branchtalk.mappings._
-import io.branchtalk.shared.model._
+import io.branchtalk.mappings.*
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.infrastructure.*
import io.branchtalk.users.UsersFixtures
-import io.scalaland.chimney.dsl._
-import monocle.macros.syntax.lens._
+import io.scalaland.chimney.dsl.*
import org.specs2.mutable.Specification
import sttp.model.StatusCode
-final class PostServerSpec extends Specification with ServerIOTest with UsersFixtures with DiscussionsFixtures {
+final class PostServerSpec extends Specification, ServerIOTest, UsersFixtures, DiscussionsFixtures {
- implicit protected lazy val uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
+ protected given uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
"PostServer-provided endpoints" should {
@@ -27,25 +28,25 @@ final class PostServerSpec extends Specification with ServerIOTest with UsersFix
CreationScheduled(channelID) <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel)
_ <- discussionsReads.channelReads.requireById(channelID).eventually()
postIDs <- (0 until 10).toList.traverse(_ =>
- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.id)
+ postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.unwrap)
)
posts <- postIDs.traverse(discussionsReads.postReads.requireById(_)).eventually()
// when
- response1 <- PostAPIs.newest.toTestCall.untupled(None, channelID, None, PaginationLimit(5).some)
+ response1 <- PostAPIs.newest.toTestCall.untupled(None, channelID, None, Pagination.Limit(5).some)
response2 <- PostAPIs.newest.toTestCall.untupled(None,
channelID,
- PaginationOffset(5L).some,
- PaginationLimit(5).some
+ Pagination.Offset(5L).some,
+ Pagination.Limit(5).some
)
} yield {
// then
- response1.code must_=== StatusCode.Ok
- response1.body must beValid(beRight(anInstanceOf[Pagination[APIPost]]))
- response2.code must_=== StatusCode.Ok
- response2.body must beValid(beRight(anInstanceOf[Pagination[APIPost]]))
+ response1.code === StatusCode.Ok
+ response1.body must beValid(beRight(beAnInstanceOf[Pagination[APIPost]]))
+ response2.code === StatusCode.Ok
+ response2.body must beValid(beRight(beAnInstanceOf[Pagination[APIPost]]))
(response1.body.toValidOpt.flatMap(_.toOption), response2.body.toValidOpt.flatMap(_.toOption))
.mapN { (pagination1, pagination2) =>
- (pagination1.entities.toSet ++ pagination2.entities.toSet) must_=== posts.map(APIPost.fromDomain).toSet
+ (pagination1.entities.toSet ++ pagination2.entities.toSet) === posts.map(APIPost.fromDomain).toSet
}
.getOrElse(pass)
}
@@ -60,18 +61,18 @@ final class PostServerSpec extends Specification with ServerIOTest with UsersFix
CreationScheduled(channelID) <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel)
_ <- discussionsReads.channelReads.requireById(channelID).eventually()
postIDs <- (0 until 10).toList.traverse(_ =>
- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.id)
+ postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.unwrap)
)
posts <- postIDs.traverse(discussionsReads.postReads.requireById(_)).eventually()
// when
response <- PostAPIs.hottest.toTestCall.untupled(None, channelID)
} yield {
// then
- response.code must_=== StatusCode.Ok
- response.body must beValid(beRight(anInstanceOf[Pagination[APIPost]]))
+ response.code === StatusCode.Ok
+ response.body must beValid(beRight(beAnInstanceOf[Pagination[APIPost]]))
response.body.toValidOpt
.flatMap(_.toOption)
- .map(pagination => pagination.entities.toSet must_=== posts.map(APIPost.fromDomain).toSet)
+ .map(pagination => pagination.entities.toSet === posts.map(APIPost.fromDomain).toSet)
.getOrElse(pass)
}
}
@@ -85,18 +86,18 @@ final class PostServerSpec extends Specification with ServerIOTest with UsersFix
CreationScheduled(channelID) <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel)
_ <- discussionsReads.channelReads.requireById(channelID).eventually()
postIDs <- (0 until 10).toList.traverse(_ =>
- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.id)
+ postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.unwrap)
)
posts <- postIDs.traverse(discussionsReads.postReads.requireById(_)).eventually()
// when
response <- PostAPIs.controversial.toTestCall.untupled(None, channelID)
} yield {
// then
- response.code must_=== StatusCode.Ok
- response.body must beValid(beRight(anInstanceOf[Pagination[APIPost]]))
+ response.code === StatusCode.Ok
+ response.body must beValid(beRight(beAnInstanceOf[Pagination[APIPost]]))
response.body.toValidOpt
.flatMap(_.toOption)
- .map(_.entities.toSet must_=== posts.map(APIPost.fromDomain).toSet)
+ .map(_.entities.toSet === posts.map(APIPost.fromDomain).toSet)
.getOrElse(pass)
}
}
@@ -124,8 +125,8 @@ final class PostServerSpec extends Specification with ServerIOTest with UsersFix
// TODO: check that this creates a new post eventually!
} yield {
// then
- response.code must_=== StatusCode.Ok
- response.body must beValid(beRight(anInstanceOf[CreatePostResponse]))
+ response.code === StatusCode.Ok
+ response.body must beValid(beRight(beAnInstanceOf[CreatePostResponse]))
}
}
}
@@ -152,8 +153,8 @@ final class PostServerSpec extends Specification with ServerIOTest with UsersFix
)
} yield {
// then
- response.code must_=== StatusCode.Ok
- response.body must beValid(beRight(be_===(APIPost.fromDomain(post))))
+ response.code === StatusCode.Ok
+ response.body must beValid(beRight(be_==(APIPost.fromDomain(post))))
}
}
}
@@ -171,10 +172,10 @@ final class PostServerSpec extends Specification with ServerIOTest with UsersFix
CreationScheduled(channelID) <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel)
_ <- discussionsReads.channelReads.requireById(channelID).eventually()
CreationScheduled(postID) <- postCreate(channelID)
- .map(_.focus(_.authorID).replace(userIDUsers2Discussions.get(userID))) // to own the Post
+ .map(_.modify(_.authorID).setTo(userIDUsers2Discussions.get(userID))) // to own the Post
.flatMap(discussionsWrites.postWrites.createPost)
post <- discussionsReads.postReads.requireById(postID).eventually()
- newTitle <- Post.Title.parse[IO]("new title")
+ newTitle <- ParseNewtype[IO].parse[Post.Title]("new title")
newContent = Post.Content.Text(Post.Text("lorem ipsum"))
// when
response <- PostAPIs.update.toTestCall.untupled(
@@ -192,17 +193,17 @@ final class PostServerSpec extends Specification with ServerIOTest with UsersFix
.eventually()
} yield {
// then
- response.code must_=== StatusCode.Ok
- response.body must beValid(beRight(be_===(UpdatePostResponse(postID))))
- updatedPost must_=== post
- .focus(_.data.title)
- .replace(newTitle)
- .focus(_.data.content)
- .replace(newContent)
- .focus(_.data.urlTitle)
- .replace(Post.UrlTitle("new-title"))
- .focus(_.data.lastModifiedAt)
- .replace(updatedPost.data.lastModifiedAt)
+ response.code === StatusCode.Ok
+ response.body must beValid(beRight(be_==(UpdatePostResponse(postID))))
+ updatedPost === post
+ .modify(_.data.title)
+ .setTo(newTitle)
+ .modify(_.data.content)
+ .setTo(newContent)
+ .modify(_.data.urlTitle)
+ .setTo(Post.UrlTitle("new-title"))
+ .modify(_.data.lastModifiedAt)
+ .setTo(updatedPost.data.lastModifiedAt)
}
}
}
@@ -220,7 +221,7 @@ final class PostServerSpec extends Specification with ServerIOTest with UsersFix
CreationScheduled(channelID) <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel)
_ <- discussionsReads.channelReads.requireById(channelID).eventually()
CreationScheduled(postID) <- postCreate(channelID)
- .map(_.focus(_.authorID).replace(userIDUsers2Discussions.get(userID))) // to own the Post
+ .map(_.modify(_.authorID).setTo(userIDUsers2Discussions.get(userID))) // to own the Post
.flatMap(discussionsWrites.postWrites.createPost)
_ <- discussionsReads.postReads.requireById(postID).eventually()
// when
@@ -235,8 +236,8 @@ final class PostServerSpec extends Specification with ServerIOTest with UsersFix
.eventually()
} yield {
// then
- response.code must_=== StatusCode.Ok
- response.body must beValid(beRight(be_===(DeletePostResponse(postID))))
+ response.code === StatusCode.Ok
+ response.body must beValid(beRight(be_==(DeletePostResponse(postID))))
}
}
}
@@ -254,7 +255,7 @@ final class PostServerSpec extends Specification with ServerIOTest with UsersFix
CreationScheduled(channelID) <- channelCreate.flatMap(discussionsWrites.channelWrites.createChannel)
_ <- discussionsReads.channelReads.requireById(channelID).eventually()
CreationScheduled(postID) <- postCreate(channelID)
- .map(_.focus(_.authorID).replace(userIDUsers2Discussions.get(userID))) // to own the Post
+ .map(_.modify(_.authorID).setTo(userIDUsers2Discussions.get(userID))) // to own the Post
.flatMap(discussionsWrites.postWrites.createPost)
_ <- discussionsReads.postReads.requireById(postID).eventually()
_ <- discussionsWrites.postWrites.deletePost(Post.Delete(postID, userIDUsers2Discussions.get(userID)))
@@ -271,8 +272,8 @@ final class PostServerSpec extends Specification with ServerIOTest with UsersFix
.eventually()
} yield {
// then
- response.code must_=== StatusCode.Ok
- response.body must beValid(beRight(be_===(RestorePostResponse(postID))))
+ response.code === StatusCode.Ok
+ response.body must beValid(beRight(be_==(RestorePostResponse(postID))))
}
}
}
@@ -299,11 +300,11 @@ final class PostServerSpec extends Specification with ServerIOTest with UsersFix
)
_ <- discussionsReads.postReads
.requireById(postID)
- .assert("Upvoted entity should have changed score")(_.data.totalScore.toInt =!= 0)
+ .assert("Upvoted entity should have changed score")(_.data.totalScore.unwrap =!= 0)
.eventually()
} yield
// then
- response.code must_=== StatusCode.Ok
+ response.code === StatusCode.Ok
}
}
@@ -329,11 +330,11 @@ final class PostServerSpec extends Specification with ServerIOTest with UsersFix
)
_ <- discussionsReads.postReads
.requireById(postID)
- .assert("Downvoted entity should have changed score")(_.data.totalScore.toInt =!= 0)
+ .assert("Downvoted entity should have changed score")(_.data.totalScore.unwrap =!= 0)
.eventually()
} yield
// then
- response.code must_=== StatusCode.Ok
+ response.code === StatusCode.Ok
}
}
@@ -355,7 +356,7 @@ final class PostServerSpec extends Specification with ServerIOTest with UsersFix
_ <- discussionsWrites.postWrites.upvotePost(Post.Upvote(postID, userIDUsers2Discussions.get(userID)))
_ <- discussionsReads.postReads
.requireById(postID)
- .assert("Upvoted entity should have changed score")(_.data.totalScore.toInt =!= 0)
+ .assert("Upvoted entity should have changed score")(_.data.totalScore.unwrap =!= 0)
.eventually()
// when
response <- PostAPIs.revokeVote.toTestCall.untupled(
@@ -365,11 +366,11 @@ final class PostServerSpec extends Specification with ServerIOTest with UsersFix
)
_ <- discussionsReads.postReads
.requireById(postID)
- .assert("Revoked-vote entity should have changed score")(_.data.totalScore.toInt === 0)
+ .assert("Revoked-vote entity should have changed score")(_.data.totalScore.unwrap eqv 0)
.eventually()
} yield
// then
- response.code must_=== StatusCode.Ok
+ response.code === StatusCode.Ok
}
}
}
diff --git a/modules/server/src/it/scala/io/branchtalk/discussions/api/SubscriptionServerSpec.scala b/modules/server/src/test/scala/io/branchtalk/discussions/api/SubscriptionServerSpec.scala
similarity index 78%
rename from modules/server/src/it/scala/io/branchtalk/discussions/api/SubscriptionServerSpec.scala
rename to modules/server/src/test/scala/io/branchtalk/discussions/api/SubscriptionServerSpec.scala
index 168eb236..200e2b90 100644
--- a/modules/server/src/it/scala/io/branchtalk/discussions/api/SubscriptionServerSpec.scala
+++ b/modules/server/src/test/scala/io/branchtalk/discussions/api/SubscriptionServerSpec.scala
@@ -1,9 +1,9 @@
package io.branchtalk.discussions.api
import cats.effect.IO
-import io.branchtalk.api.{ Authentication, Pagination, PaginationLimit, PaginationOffset, ServerIOTest }
+import io.branchtalk.api.{ Authentication, Pagination, ServerIOTest }
import io.branchtalk.discussions.DiscussionsFixtures
-import io.branchtalk.discussions.api.PostModels._
+import io.branchtalk.discussions.api.PostModels.*
import io.branchtalk.discussions.api.SubscriptionModels.{
APISubscriptions,
SubscribeRequest,
@@ -12,17 +12,18 @@ import io.branchtalk.discussions.api.SubscriptionModels.{
UnsubscribeResponse
}
import io.branchtalk.discussions.model.{ Channel, Subscription }
-import io.branchtalk.mappings._
-import io.branchtalk.shared.model._
+import io.branchtalk.mappings.*
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.infrastructure.*
import io.branchtalk.users.UsersFixtures
import org.specs2.mutable.Specification
import sttp.model.StatusCode
-final class SubscriptionServerSpec extends Specification with ServerIOTest with UsersFixtures with DiscussionsFixtures {
+final class SubscriptionServerSpec extends Specification, ServerIOTest, UsersFixtures, DiscussionsFixtures {
private val defaultChannelID = ID[Channel](java.util.UUID.randomUUID())
- implicit protected lazy val uuidGenerator: TestUUIDGenerator =
- (new TestUUIDGenerator).tap(_.stubNext(defaultChannelID.uuid)) // stub generation in ServerIOTest resources
+ protected given uuidGenerator: TestUUIDGenerator =
+ (new TestUUIDGenerator).tap(_.stubNext(defaultChannelID.unwrap)) // stub generation in ServerIOTest resources
"SubscriptionServer-provided endpoints" should {
@@ -32,29 +33,29 @@ final class SubscriptionServerSpec extends Specification with ServerIOTest with
for {
// given
CreationScheduled(channelID) <- channelCreate
- .flatTap(_ => IO(uuidGenerator.stubNext(defaultChannelID.uuid))) // create Channel with default ID
+ .flatTap(_ => IO(uuidGenerator.stubNext(defaultChannelID.unwrap))) // create Channel with default ID
.flatMap(discussionsWrites.channelWrites.createChannel) // NOTE: ID generation must come before CID
- .assert("Created Channel should have predefined ID")(_.id === defaultChannelID)
+ .assert("Created Channel should have predefined ID")(_.unwrap eqv defaultChannelID)
_ <- discussionsReads.channelReads.requireById(channelID).eventually()
postIDs <- (0 until 10).toList.traverse(_ =>
- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.id)
+ postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.unwrap)
)
posts <- postIDs.traverse(discussionsReads.postReads.requireById(_)).eventually()
// when
- response1 <- SubscriptionAPIs.newest.toTestCall.untupled(None, None, PaginationLimit(5).some)
+ response1 <- SubscriptionAPIs.newest.toTestCall.untupled(None, None, Pagination.Limit(5).some)
response2 <- SubscriptionAPIs.newest.toTestCall.untupled(None,
- PaginationOffset(5L).some,
- PaginationLimit(5).some
+ Pagination.Offset(5L).some,
+ Pagination.Limit(5).some
)
} yield {
// then
- response1.code must_=== StatusCode.Ok
- response1.body must beValid(beRight(anInstanceOf[Pagination[APIPost]]))
- response2.code must_=== StatusCode.Ok
- response2.body must beValid(beRight(anInstanceOf[Pagination[APIPost]]))
+ response1.code === StatusCode.Ok
+ response1.body must beValid(beRight(beAnInstanceOf[Pagination[APIPost]]))
+ response2.code === StatusCode.Ok
+ response2.body must beValid(beRight(beAnInstanceOf[Pagination[APIPost]]))
(response1.body.toValidOpt.flatMap(_.toOption), response2.body.toValidOpt.flatMap(_.toOption))
.mapN { (pagination1, pagination2) =>
- (pagination1.entities.toSet ++ pagination2.entities.toSet) must_=== posts.map(APIPost.fromDomain).toSet
+ (pagination1.entities.toSet ++ pagination2.entities.toSet) === posts.map(APIPost.fromDomain).toSet
}
.getOrElse(pass)
}
@@ -79,29 +80,29 @@ final class SubscriptionServerSpec extends Specification with ServerIOTest with
.assert("Subscriptions should contain added Channel ID")(_.subscriptions(channelID))
.eventually()
postIDs <- (0 until 10).toList.traverse(_ =>
- postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.id)
+ postCreate(channelID).flatMap(discussionsWrites.postWrites.createPost).map(_.unwrap)
)
posts <- postIDs.traverse(discussionsReads.postReads.requireById(_)).eventually()
// when
response1 <- SubscriptionAPIs.newest.toTestCall.untupled(
Authentication.Session(sessionID = sessionIDApi2Users.reverseGet(sessionID)).some,
None,
- PaginationLimit(5).some
+ Pagination.Limit(5).some
)
response2 <- SubscriptionAPIs.newest.toTestCall.untupled(
Authentication.Session(sessionID = sessionIDApi2Users.reverseGet(sessionID)).some,
- PaginationOffset(5L).some,
- PaginationLimit(5).some
+ Pagination.Offset(5L).some,
+ Pagination.Limit(5).some
)
} yield {
// then
- response1.code must_=== StatusCode.Ok
- response1.body must beValid(beRight(anInstanceOf[Pagination[APIPost]]))
- response2.code must_=== StatusCode.Ok
- response2.body must beValid(beRight(anInstanceOf[Pagination[APIPost]]))
+ response1.code === StatusCode.Ok
+ response1.body must beValid(beRight(beAnInstanceOf[Pagination[APIPost]]))
+ response2.code === StatusCode.Ok
+ response2.body must beValid(beRight(beAnInstanceOf[Pagination[APIPost]]))
(response1.body.toValidOpt.flatMap(_.toOption), response2.body.toValidOpt.flatMap(_.toOption))
.mapN { (pagination1, pagination2) =>
- (pagination1.entities.toSet ++ pagination2.entities.toSet) must_=== posts.map(APIPost.fromDomain).toSet
+ (pagination1.entities.toSet ++ pagination2.entities.toSet) === posts.map(APIPost.fromDomain).toSet
}
.getOrElse(pass)
}
@@ -134,11 +135,11 @@ final class SubscriptionServerSpec extends Specification with ServerIOTest with
)
} yield {
// then
- response.code must_=== StatusCode.Ok
- response.body must beValid(beRight(anInstanceOf[APISubscriptions]))
+ response.code === StatusCode.Ok
+ response.body must beValid(beRight(beAnInstanceOf[APISubscriptions]))
response.body.toValidOpt
.flatMap(_.toOption)
- .map(subscriptions => subscriptions must_=== APISubscriptions(List(channelID)))
+ .map(subscriptions => subscriptions === APISubscriptions(List(channelID)))
.getOrElse(pass)
}
}
@@ -168,13 +169,13 @@ final class SubscriptionServerSpec extends Specification with ServerIOTest with
.eventually()
} yield {
// then
- response.code must_=== StatusCode.Ok
- response.body must beValid(beRight(anInstanceOf[SubscribeResponse]))
+ response.code === StatusCode.Ok
+ response.body must beValid(beRight(beAnInstanceOf[SubscribeResponse]))
response.body.toValidOpt
.flatMap(_.toOption)
- .map(subscribed => subscribed.channels must_=== List(channelID))
+ .map(subscribed => subscribed.channels === List(channelID))
.getOrElse(pass)
- result must_=== Subscription(subscriberID, Set(channelID))
+ result === Subscription(subscriberID, Set(channelID))
}
}
}
@@ -210,13 +211,13 @@ final class SubscriptionServerSpec extends Specification with ServerIOTest with
.eventually()
} yield {
// then
- response.code must_=== StatusCode.Ok
- response.body must beValid(beRight(anInstanceOf[UnsubscribeResponse]))
+ response.code === StatusCode.Ok
+ response.body must beValid(beRight(beAnInstanceOf[UnsubscribeResponse]))
response.body.toValidOpt
.flatMap(_.toOption)
- .map(unsubscribed => unsubscribed must_=== UnsubscribeResponse(List()))
+ .map(unsubscribed => unsubscribed === UnsubscribeResponse(List()))
.getOrElse(pass)
- result must_=== Subscription(subscriberID, Set.empty)
+ result === Subscription(subscriberID, Set.empty)
}
}
}
diff --git a/modules/server/src/it/scala/io/branchtalk/openapi/OpenAPIServerSpec.scala b/modules/server/src/test/scala/io/branchtalk/openapi/OpenAPIServerSpec.scala
similarity index 63%
rename from modules/server/src/it/scala/io/branchtalk/openapi/OpenAPIServerSpec.scala
rename to modules/server/src/test/scala/io/branchtalk/openapi/OpenAPIServerSpec.scala
index 24163490..45c6543c 100644
--- a/modules/server/src/it/scala/io/branchtalk/openapi/OpenAPIServerSpec.scala
+++ b/modules/server/src/test/scala/io/branchtalk/openapi/OpenAPIServerSpec.scala
@@ -4,18 +4,18 @@ import io.branchtalk.api.ServerIOTest
import io.branchtalk.shared.model.TestUUIDGenerator
import org.specs2.mutable.Specification
import sttp.model.StatusCode
-import sttp.client3._
+import sttp.client3.*
-final class OpenAPIServerSpec extends Specification with ServerIOTest {
+final class OpenAPIServerSpec extends Specification, ServerIOTest {
- implicit protected val uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
+ protected given uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
"OpenAPIServer" should {
"return valid OpenAPI v3 specification" in {
for {
result <- basicRequest.get(sttpBaseUri.withWholePath("docs/swagger.json")).send(client)
- } yield result.code must_=== StatusCode.Ok
+ } yield result.code === StatusCode.Ok
}
}
}
diff --git a/modules/server/src/it/scala/io/branchtalk/users/api/ChannelBanServerListingSpec.scala b/modules/server/src/test/scala/io/branchtalk/users/api/ChannelBanServerListingSpec.scala
similarity index 75%
rename from modules/server/src/it/scala/io/branchtalk/users/api/ChannelBanServerListingSpec.scala
rename to modules/server/src/test/scala/io/branchtalk/users/api/ChannelBanServerListingSpec.scala
index 57586196..241e9b1a 100644
--- a/modules/server/src/it/scala/io/branchtalk/users/api/ChannelBanServerListingSpec.scala
+++ b/modules/server/src/test/scala/io/branchtalk/users/api/ChannelBanServerListingSpec.scala
@@ -1,25 +1,22 @@
package io.branchtalk.users.api
-import io.branchtalk.api.{ Permission => _, RequiredPermissions => _, _ }
+import io.branchtalk.api.{ Permission => _, RequiredPermissions => _, * }
import io.branchtalk.discussions.DiscussionsFixtures
-import io.branchtalk.mappings._
-import io.branchtalk.shared.model._
+import io.branchtalk.mappings.*
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.infrastructure.*
import io.branchtalk.users.UsersFixtures
-import io.branchtalk.users.api.UserModels._
+import io.branchtalk.users.api.UserModels.*
import io.branchtalk.users.model.{ Ban, Permission, User }
import org.specs2.mutable.Specification
import sttp.model.StatusCode
-final class ChannelBanServerListingSpec
- extends Specification
- with ServerIOTest
- with UsersFixtures
- with DiscussionsFixtures {
+final class ChannelBanServerListingSpec extends Specification, ServerIOTest, UsersFixtures, DiscussionsFixtures {
// User pagination tests cannot be run in parallel to other User tests (no parent to filter other tests)
sequential
- implicit protected val uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
+ protected given uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
"ChannelBanServer-provided pagination endpoints" should {
@@ -43,7 +40,7 @@ final class ChannelBanServerListingSpec
)
)
bannedUserIDs <- (0 until 9).toList.traverse(_ =>
- userCreate.flatMap(usersWrites.userWrites.createUser).map(_._1.id)
+ userCreate.flatMap(usersWrites.userWrites.createUser).map(_._1.unwrap)
)
_ <- bannedUserIDs.traverse(usersReads.userReads.requireById(_)).eventually()
channelID <- channelIDCreate
@@ -53,7 +50,7 @@ final class ChannelBanServerListingSpec
.traverse(usersWrites.banWrites.orderBan)
_ <- usersReads.banReads
.findForChannel(channelID)
- .assert("All Users should be Banned")(_.map(_.bannedUserID).toSet === bannedUserIDs.toSet)
+ .assert("All Users should be Banned")(_.map(_.bannedUserID).toSet eqv bannedUserIDs.toSet)
.eventually()
// when
response <- ChannelBanAPIs.list.toTestCall.untupled(
@@ -62,12 +59,9 @@ final class ChannelBanServerListingSpec
)
} yield {
// then
- response.code must_=== StatusCode.Ok
- response.body must beValid(beRight(anInstanceOf[BansResponse]))
- response.body.toValidOpt
- .flatMap(_.toOption)
- .map(_.bannedIDs.toSet must_=== bannedUserIDs.toSet)
- .getOrElse(pass)
+ response.code === StatusCode.Ok
+ response.body must beValid(beRight(beAnInstanceOf[BansResponse]))
+ response.body.toValidOpt.flatMap(_.toOption).map(_.bannedIDs.toSet === bannedUserIDs.toSet).getOrElse(pass)
}
}
}
diff --git a/modules/server/src/it/scala/io/branchtalk/users/api/ChannelBanServerSpec.scala b/modules/server/src/test/scala/io/branchtalk/users/api/ChannelBanServerSpec.scala
similarity index 85%
rename from modules/server/src/it/scala/io/branchtalk/users/api/ChannelBanServerSpec.scala
rename to modules/server/src/test/scala/io/branchtalk/users/api/ChannelBanServerSpec.scala
index 32fcb9eb..ea1dcd8c 100644
--- a/modules/server/src/it/scala/io/branchtalk/users/api/ChannelBanServerSpec.scala
+++ b/modules/server/src/test/scala/io/branchtalk/users/api/ChannelBanServerSpec.scala
@@ -1,17 +1,18 @@
package io.branchtalk.users.api
-import io.branchtalk.api.{ Permission => _, RequiredPermissions => _, _ }
+import io.branchtalk.api.{ Permission => _, RequiredPermissions => _, * }
import io.branchtalk.discussions.DiscussionsFixtures
-import io.branchtalk.mappings._
-import io.branchtalk.shared.model._
+import io.branchtalk.mappings.*
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.infrastructure.*
import io.branchtalk.users.UsersFixtures
-import io.branchtalk.users.api.UserModels._
+import io.branchtalk.users.api.UserModels.*
import io.branchtalk.users.model.{ Ban, Permission, User }
import org.specs2.mutable.Specification
-final class ChannelBanServerSpec extends Specification with ServerIOTest with UsersFixtures with DiscussionsFixtures {
+final class ChannelBanServerSpec extends Specification, ServerIOTest, UsersFixtures, DiscussionsFixtures {
- implicit protected val uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
+ protected given uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
"ChannelBanServer-provided endpoints" should {
@@ -52,8 +53,8 @@ final class ChannelBanServerSpec extends Specification with ServerIOTest with Us
.eventually()
} yield {
// then
- response.body must beValid(beRight(be_===(BanOrderResponse(bannedUserID))))
- bans must_=== Set(Ban(bannedUserID, reason, Ban.Scope.ForChannel(channelID)))
+ response.body must beValid(beRight(be_==(BanOrderResponse(bannedUserID))))
+ bans === Set(Ban(bannedUserID, reason, Ban.Scope.ForChannel(channelID)))
}
}
@@ -99,7 +100,7 @@ final class ChannelBanServerSpec extends Specification with ServerIOTest with Us
.eventually()
} yield
// then
- response.body must beValid(beRight(be_===(BanLiftResponse(bannedUserID))))
+ response.body must beValid(beRight(be_==(BanLiftResponse(bannedUserID))))
}
}
}
diff --git a/modules/server/src/it/scala/io/branchtalk/users/api/ChannelModerationServerPaginationSpec.scala b/modules/server/src/test/scala/io/branchtalk/users/api/ChannelModerationServerPaginationSpec.scala
similarity index 79%
rename from modules/server/src/it/scala/io/branchtalk/users/api/ChannelModerationServerPaginationSpec.scala
rename to modules/server/src/test/scala/io/branchtalk/users/api/ChannelModerationServerPaginationSpec.scala
index de099656..341d04fd 100644
--- a/modules/server/src/it/scala/io/branchtalk/users/api/ChannelModerationServerPaginationSpec.scala
+++ b/modules/server/src/test/scala/io/branchtalk/users/api/ChannelModerationServerPaginationSpec.scala
@@ -1,25 +1,26 @@
package io.branchtalk.users.api
-import io.branchtalk.api.{ Permission => _, RequiredPermissions => _, _ }
+import io.branchtalk.api.{ Permission => _, RequiredPermissions => _, * }
import io.branchtalk.discussions.DiscussionsFixtures
-import io.branchtalk.mappings._
-import io.branchtalk.shared.model._
+import io.branchtalk.mappings.*
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.infrastructure.*
import io.branchtalk.users.UsersFixtures
-import io.branchtalk.users.api.UserModels._
+import io.branchtalk.users.api.UserModels.*
import io.branchtalk.users.model.{ Permission, RequiredPermissions, User }
import org.specs2.mutable.Specification
import sttp.model.StatusCode
final class ChannelModerationServerPaginationSpec
- extends Specification
- with ServerIOTest
- with UsersFixtures
- with DiscussionsFixtures {
+ extends Specification,
+ ServerIOTest,
+ UsersFixtures,
+ DiscussionsFixtures {
// User pagination tests cannot be run in parallel to other User tests (no parent to filter other tests)
sequential
- implicit protected val uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
+ protected given uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
"ChannelModerationServer-provided pagination endpoints" should {
@@ -43,7 +44,7 @@ final class ChannelModerationServerPaginationSpec
)
)
userIDs <- (0 until 9).toList
- .traverse(_ => userCreate.flatMap(usersWrites.userWrites.createUser).map(_._1.id))
+ .traverse(_ => userCreate.flatMap(usersWrites.userWrites.createUser).map(_._1.unwrap))
.map(_ :+ userID)
_ <- userIDs.traverse(usersReads.userReads.requireById(_)).eventually()
channelID <- channelIDCreate
@@ -71,23 +72,23 @@ final class ChannelModerationServerPaginationSpec
Authentication.Session(sessionID = sessionIDApi2Users.reverseGet(sessionID)),
channelID,
None,
- PaginationLimit(5).some
+ Pagination.Limit(5).some
)
response2 <- ChannelModerationAPIs.paginate.toTestCall.untupled(
Authentication.Session(sessionID = sessionIDApi2Users.reverseGet(sessionID)),
channelID,
- PaginationOffset(5L).some,
- PaginationLimit(5).some
+ Pagination.Offset(5L).some,
+ Pagination.Limit(5).some
)
} yield {
// then
- response1.code must_=== StatusCode.Ok
- response1.body must beValid(beRight(anInstanceOf[Pagination[APIUser]]))
- response2.code must_=== StatusCode.Ok
- response2.body must beValid(beRight(anInstanceOf[Pagination[APIUser]]))
+ response1.code === StatusCode.Ok
+ response1.body must beValid(beRight(beAnInstanceOf[Pagination[APIUser]]))
+ response2.code === StatusCode.Ok
+ response2.body must beValid(beRight(beAnInstanceOf[Pagination[APIUser]]))
(response1.body.toValidOpt.flatMap(_.toOption), response2.body.toValidOpt.flatMap(_.toOption))
.mapN { (pagination1, pagination2) =>
- (pagination1.entities.toSet ++ pagination2.entities.toSet) must_=== users.map(APIUser.fromDomain).toSet
+ (pagination1.entities.toSet ++ pagination2.entities.toSet) === users.map(APIUser.fromDomain).toSet
}
.getOrElse(pass)
}
diff --git a/modules/server/src/it/scala/io/branchtalk/users/api/ChannelModerationServerSpec.scala b/modules/server/src/test/scala/io/branchtalk/users/api/ChannelModerationServerSpec.scala
similarity index 89%
rename from modules/server/src/it/scala/io/branchtalk/users/api/ChannelModerationServerSpec.scala
rename to modules/server/src/test/scala/io/branchtalk/users/api/ChannelModerationServerSpec.scala
index 9dbb6f58..7eb81c89 100644
--- a/modules/server/src/it/scala/io/branchtalk/users/api/ChannelModerationServerSpec.scala
+++ b/modules/server/src/test/scala/io/branchtalk/users/api/ChannelModerationServerSpec.scala
@@ -1,21 +1,18 @@
package io.branchtalk.users.api
-import io.branchtalk.api.{ Permission => _, RequiredPermissions => _, _ }
+import io.branchtalk.api.{ Permission => _, RequiredPermissions => _, * }
import io.branchtalk.discussions.DiscussionsFixtures
-import io.branchtalk.mappings._
-import io.branchtalk.shared.model._
+import io.branchtalk.mappings.*
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.infrastructure.*
import io.branchtalk.users.UsersFixtures
-import io.branchtalk.users.api.UserModels._
+import io.branchtalk.users.api.UserModels.*
import io.branchtalk.users.model.{ Permission, RequiredPermissions, User }
import org.specs2.mutable.Specification
-final class ChannelModerationServerSpec
- extends Specification
- with ServerIOTest
- with UsersFixtures
- with DiscussionsFixtures {
+final class ChannelModerationServerSpec extends Specification, ServerIOTest, UsersFixtures, DiscussionsFixtures {
- implicit protected val uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
+ protected given uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
"ChannelModerationServer-provided endpoints" should {
@@ -58,7 +55,7 @@ final class ChannelModerationServerSpec
.eventually()
} yield
// then
- response.body must beValid(beRight(be_===(GrantModerationResponse(updatedUserID))))
+ response.body must beValid(beRight(be_==(GrantModerationResponse(updatedUserID))))
}
"on DELETE /discussions/channels/{channelID}/moderation" in {
@@ -114,7 +111,7 @@ final class ChannelModerationServerSpec
.eventually()
} yield
// then
- response.body must beValid(beRight(be_===(RevokeModerationResponse(updatedUserID))))
+ response.body must beValid(beRight(be_==(RevokeModerationResponse(updatedUserID))))
}
}
}
diff --git a/modules/server/src/it/scala/io/branchtalk/users/api/UserBanServerListingSpec.scala b/modules/server/src/test/scala/io/branchtalk/users/api/UserBanServerListingSpec.scala
similarity index 75%
rename from modules/server/src/it/scala/io/branchtalk/users/api/UserBanServerListingSpec.scala
rename to modules/server/src/test/scala/io/branchtalk/users/api/UserBanServerListingSpec.scala
index 6d76ae42..766d64db 100644
--- a/modules/server/src/it/scala/io/branchtalk/users/api/UserBanServerListingSpec.scala
+++ b/modules/server/src/test/scala/io/branchtalk/users/api/UserBanServerListingSpec.scala
@@ -2,24 +2,21 @@ package io.branchtalk.users.api
import io.branchtalk.api.{ Permission => _, RequiredPermissions => _, _ }
import io.branchtalk.discussions.DiscussionsFixtures
-import io.branchtalk.mappings._
-import io.branchtalk.shared.model._
+import io.branchtalk.mappings.*
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.infrastructure.*
import io.branchtalk.users.UsersFixtures
-import io.branchtalk.users.api.UserModels._
+import io.branchtalk.users.api.UserModels.*
import io.branchtalk.users.model.{ Ban, Permission, User }
import org.specs2.mutable.Specification
import sttp.model.StatusCode
-final class UserBanServerListingSpec
- extends Specification
- with ServerIOTest
- with UsersFixtures
- with DiscussionsFixtures {
+final class UserBanServerListingSpec extends Specification, ServerIOTest, UsersFixtures, DiscussionsFixtures {
// User pagination tests cannot be run in parallel to other User tests (no parent to filter other tests)
sequential
- implicit protected val uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
+ protected given uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
"UserBanServer-provided pagination endpoints" should {
@@ -43,7 +40,7 @@ final class UserBanServerListingSpec
)
)
bannedUserIDs <- (0 until 9).toList.traverse(_ =>
- userCreate.flatMap(usersWrites.userWrites.createUser).map(_._1.id)
+ userCreate.flatMap(usersWrites.userWrites.createUser).map(_._1.unwrap)
)
_ <- bannedUserIDs.traverse(usersReads.userReads.requireById(_)).eventually()
reason = Ban.Reason("test")
@@ -51,7 +48,7 @@ final class UserBanServerListingSpec
.map(Ban.Order(_, reason, Ban.Scope.Globally, userID.some))
.traverse(usersWrites.banWrites.orderBan)
_ <- usersReads.banReads.findGlobally
- .assert("All Users should be Banned")(_.map(_.bannedUserID).toSet === bannedUserIDs.toSet)
+ .assert("All Users should be Banned")(_.map(_.bannedUserID).toSet eqv bannedUserIDs.toSet)
.eventually()
// when
response <- UserBanAPIs.list.toTestCall(
@@ -59,12 +56,9 @@ final class UserBanServerListingSpec
)
} yield {
// then
- response.code must_=== StatusCode.Ok
- response.body must beValid(beRight(anInstanceOf[BansResponse]))
- response.body.toValidOpt
- .flatMap(_.toOption)
- .map(_.bannedIDs.toSet must_=== bannedUserIDs.toSet)
- .getOrElse(pass)
+ response.code === StatusCode.Ok
+ response.body must beValid(beRight(beAnInstanceOf[BansResponse]))
+ response.body.toValidOpt.flatMap(_.toOption).map(_.bannedIDs.toSet === bannedUserIDs.toSet).getOrElse(pass)
}
}
}
diff --git a/modules/server/src/it/scala/io/branchtalk/users/api/UserBanServerSpec.scala b/modules/server/src/test/scala/io/branchtalk/users/api/UserBanServerSpec.scala
similarity index 85%
rename from modules/server/src/it/scala/io/branchtalk/users/api/UserBanServerSpec.scala
rename to modules/server/src/test/scala/io/branchtalk/users/api/UserBanServerSpec.scala
index 02ddcfab..96904e17 100644
--- a/modules/server/src/it/scala/io/branchtalk/users/api/UserBanServerSpec.scala
+++ b/modules/server/src/test/scala/io/branchtalk/users/api/UserBanServerSpec.scala
@@ -2,16 +2,17 @@ package io.branchtalk.users.api
import io.branchtalk.api.{ Permission => _, RequiredPermissions => _, _ }
import io.branchtalk.discussions.DiscussionsFixtures
-import io.branchtalk.mappings._
-import io.branchtalk.shared.model._
+import io.branchtalk.mappings.*
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.infrastructure.*
import io.branchtalk.users.UsersFixtures
-import io.branchtalk.users.api.UserModels._
+import io.branchtalk.users.api.UserModels.*
import io.branchtalk.users.model.{ Ban, Permission, User }
import org.specs2.mutable.Specification
-final class UserBanServerSpec extends Specification with ServerIOTest with UsersFixtures with DiscussionsFixtures {
+final class UserBanServerSpec extends Specification, ServerIOTest, UsersFixtures, DiscussionsFixtures {
- implicit protected val uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
+ protected given uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
"UserBanServer-provided endpoints" should {
@@ -50,8 +51,8 @@ final class UserBanServerSpec extends Specification with ServerIOTest with Users
.eventually()
} yield {
// then
- response.body must beValid(beRight(be_===(BanOrderResponse(bannedUserID))))
- bans must_=== Set(Ban(bannedUserID, reason, Ban.Scope.Globally))
+ response.body must beValid(beRight(be_==(BanOrderResponse(bannedUserID))))
+ bans === Set(Ban(bannedUserID, reason, Ban.Scope.Globally))
}
}
@@ -95,7 +96,7 @@ final class UserBanServerSpec extends Specification with ServerIOTest with Users
.eventually()
} yield
// then
- response.body must beValid(beRight(be_===(BanLiftResponse(bannedUserID))))
+ response.body must beValid(beRight(be_==(BanLiftResponse(bannedUserID))))
}
}
}
diff --git a/modules/server/src/it/scala/io/branchtalk/users/api/UserModerationServerPaginationSpec.scala b/modules/server/src/test/scala/io/branchtalk/users/api/UserModerationServerPaginationSpec.scala
similarity index 78%
rename from modules/server/src/it/scala/io/branchtalk/users/api/UserModerationServerPaginationSpec.scala
rename to modules/server/src/test/scala/io/branchtalk/users/api/UserModerationServerPaginationSpec.scala
index 4afaccc5..07aae163 100644
--- a/modules/server/src/it/scala/io/branchtalk/users/api/UserModerationServerPaginationSpec.scala
+++ b/modules/server/src/test/scala/io/branchtalk/users/api/UserModerationServerPaginationSpec.scala
@@ -1,25 +1,22 @@
package io.branchtalk.users.api
-import io.branchtalk.api.{ Permission => _, RequiredPermissions => _, _ }
+import io.branchtalk.api.{ Permission => _, RequiredPermissions => _, * }
import io.branchtalk.discussions.DiscussionsFixtures
-import io.branchtalk.mappings._
-import io.branchtalk.shared.model._
+import io.branchtalk.mappings.*
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.infrastructure.*
import io.branchtalk.users.UsersFixtures
-import io.branchtalk.users.api.UserModels._
+import io.branchtalk.users.api.UserModels.*
import io.branchtalk.users.model.{ Permission, RequiredPermissions, User }
import org.specs2.mutable.Specification
import sttp.model.StatusCode
-final class UserModerationServerPaginationSpec
- extends Specification
- with ServerIOTest
- with UsersFixtures
- with DiscussionsFixtures {
+final class UserModerationServerPaginationSpec extends Specification, ServerIOTest, UsersFixtures, DiscussionsFixtures {
// User pagination tests cannot be run in parallel to other User tests (no parent to filter other tests)
sequential
- implicit protected val uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
+ protected given uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
"UserModerationServer-provided pagination endpoints" should {
@@ -43,7 +40,7 @@ final class UserModerationServerPaginationSpec
)
)
userIDs <- (0 until 9).toList
- .traverse(_ => userCreate.flatMap(usersWrites.userWrites.createUser).map(_._1.id))
+ .traverse(_ => userCreate.flatMap(usersWrites.userWrites.createUser).map(_._1.unwrap))
.map(_ :+ userID)
_ <- userIDs.traverse(usersReads.userReads.requireById(_)).eventually()
permission = Permission.ModerateUsers
@@ -69,22 +66,22 @@ final class UserModerationServerPaginationSpec
response1 <- UserModerationAPIs.paginate.toTestCall.untupled(
Authentication.Session(sessionID = sessionIDApi2Users.reverseGet(sessionID)),
None,
- PaginationLimit(5).some
+ Pagination.Limit(5).some
)
response2 <- UserModerationAPIs.paginate.toTestCall.untupled(
Authentication.Session(sessionID = sessionIDApi2Users.reverseGet(sessionID)),
- PaginationOffset(5L).some,
- PaginationLimit(5).some
+ Pagination.Offset(5L).some,
+ Pagination.Limit(5).some
)
} yield {
// then
- response1.code must_=== StatusCode.Ok
- response1.body must beValid(beRight(anInstanceOf[Pagination[APIUser]]))
- response2.code must_=== StatusCode.Ok
- response2.body must beValid(beRight(anInstanceOf[Pagination[APIUser]]))
+ response1.code === StatusCode.Ok
+ response1.body must beValid(beRight(beAnInstanceOf[Pagination[APIUser]]))
+ response2.code === StatusCode.Ok
+ response2.body must beValid(beRight(beAnInstanceOf[Pagination[APIUser]]))
(response1.body.toValidOpt.flatMap(_.toOption), response2.body.toValidOpt.flatMap(_.toOption))
.mapN { (pagination1, pagination2) =>
- (pagination1.entities.toSet ++ pagination2.entities.toSet) must_=== users.map(APIUser.fromDomain).toSet
+ (pagination1.entities.toSet ++ pagination2.entities.toSet) === users.map(APIUser.fromDomain).toSet
}
.getOrElse(pass)
}
diff --git a/modules/server/src/it/scala/io/branchtalk/users/api/UserModerationServerSpec.scala b/modules/server/src/test/scala/io/branchtalk/users/api/UserModerationServerSpec.scala
similarity index 88%
rename from modules/server/src/it/scala/io/branchtalk/users/api/UserModerationServerSpec.scala
rename to modules/server/src/test/scala/io/branchtalk/users/api/UserModerationServerSpec.scala
index c7a2441a..70f79a6a 100644
--- a/modules/server/src/it/scala/io/branchtalk/users/api/UserModerationServerSpec.scala
+++ b/modules/server/src/test/scala/io/branchtalk/users/api/UserModerationServerSpec.scala
@@ -1,21 +1,18 @@
package io.branchtalk.users.api
-import io.branchtalk.api.{ Permission => _, RequiredPermissions => _, _ }
+import io.branchtalk.api.{ Permission => _, RequiredPermissions => _, * }
import io.branchtalk.discussions.DiscussionsFixtures
-import io.branchtalk.mappings._
-import io.branchtalk.shared.model._
+import io.branchtalk.mappings.*
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.infrastructure.*
import io.branchtalk.users.UsersFixtures
-import io.branchtalk.users.api.UserModels._
+import io.branchtalk.users.api.UserModels.*
import io.branchtalk.users.model.{ Permission, RequiredPermissions, User }
import org.specs2.mutable.Specification
-final class UserModerationServerSpec
- extends Specification
- with ServerIOTest
- with UsersFixtures
- with DiscussionsFixtures {
+final class UserModerationServerSpec extends Specification, ServerIOTest, UsersFixtures, DiscussionsFixtures {
- implicit protected val uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
+ protected given uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
"UserModerationServer-provided endpoints" should {
@@ -56,7 +53,7 @@ final class UserModerationServerSpec
.eventually()
} yield
// then
- response.body must beValid(beRight(be_===(GrantModerationResponse(updatedUserID))))
+ response.body must beValid(beRight(be_==(GrantModerationResponse(updatedUserID))))
}
"on DELETE /users/moderation" in {
@@ -110,7 +107,7 @@ final class UserModerationServerSpec
.eventually()
} yield
// then
- response.body must beValid(beRight(be_===(RevokeModerationResponse(updatedUserID))))
+ response.body must beValid(beRight(be_==(RevokeModerationResponse(updatedUserID))))
}
}
}
diff --git a/modules/server/src/it/scala/io/branchtalk/users/api/UserServerPaginationSpec.scala b/modules/server/src/test/scala/io/branchtalk/users/api/UserServerPaginationSpec.scala
similarity index 74%
rename from modules/server/src/it/scala/io/branchtalk/users/api/UserServerPaginationSpec.scala
rename to modules/server/src/test/scala/io/branchtalk/users/api/UserServerPaginationSpec.scala
index adc47ce0..c6ed8c88 100644
--- a/modules/server/src/it/scala/io/branchtalk/users/api/UserServerPaginationSpec.scala
+++ b/modules/server/src/test/scala/io/branchtalk/users/api/UserServerPaginationSpec.scala
@@ -1,25 +1,22 @@
package io.branchtalk.users.api
-import io.branchtalk.api.{ Permission => _, RequiredPermissions => _, _ }
+import io.branchtalk.api.{ Permission => _, RequiredPermissions => _, * }
import io.branchtalk.discussions.DiscussionsFixtures
-import io.branchtalk.mappings._
-import io.branchtalk.shared.model._
+import io.branchtalk.mappings.*
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.infrastructure.*
import io.branchtalk.users.UsersFixtures
-import io.branchtalk.users.api.UserModels._
+import io.branchtalk.users.api.UserModels.*
import io.branchtalk.users.model.{ Permission, RequiredPermissions, User }
import org.specs2.mutable.Specification
import sttp.model.StatusCode
-final class UserServerPaginationSpec
- extends Specification
- with ServerIOTest
- with UsersFixtures
- with DiscussionsFixtures {
+final class UserServerPaginationSpec extends Specification, ServerIOTest, UsersFixtures, DiscussionsFixtures {
// User pagination tests cannot be run in parallel to other User tests (no parent to filter other tests)
sequential
- implicit protected val uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
+ protected given uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
"UserServer-provided pagination endpoints" should {
@@ -49,29 +46,29 @@ final class UserServerPaginationSpec
)
.eventually()
userIDs <- (0 until 9).toList
- .traverse(_ => userCreate.flatMap(usersWrites.userWrites.createUser).map(_._1.id))
+ .traverse(_ => userCreate.flatMap(usersWrites.userWrites.createUser).map(_._1.unwrap))
.map(_ :+ userID)
users <- userIDs.traverse(usersReads.userReads.requireById(_)).eventually()
// when
response1 <- UserAPIs.paginate.toTestCall.untupled(
Authentication.Session(sessionID = sessionIDApi2Users.reverseGet(sessionID)),
None,
- PaginationLimit(5).some
+ Pagination.Limit(5).some
)
response2 <- UserAPIs.paginate.toTestCall.untupled(
Authentication.Session(sessionID = sessionIDApi2Users.reverseGet(sessionID)),
- PaginationOffset(5L).some,
- PaginationLimit(5).some
+ Pagination.Offset(5L).some,
+ Pagination.Limit(5).some
)
} yield {
// then
- response1.code must_=== StatusCode.Ok
- response1.body must beValid(beRight(anInstanceOf[Pagination[APIUser]]))
- response2.code must_=== StatusCode.Ok
- response2.body must beValid(beRight(anInstanceOf[Pagination[APIUser]]))
+ response1.code === StatusCode.Ok
+ response1.body must beValid(beRight(beAnInstanceOf[Pagination[APIUser]]))
+ response2.code === StatusCode.Ok
+ response2.body must beValid(beRight(beAnInstanceOf[Pagination[APIUser]]))
(response1.body.toValidOpt.flatMap(_.toOption), response2.body.toValidOpt.flatMap(_.toOption))
.mapN { (pagination1, pagination2) =>
- (pagination1.entities.toSet ++ pagination2.entities.toSet) must_=== users.map(APIUser.fromDomain).toSet
+ (pagination1.entities.toSet ++ pagination2.entities.toSet) === users.map(APIUser.fromDomain).toSet
}
.getOrElse(pass)
}
@@ -83,10 +80,11 @@ final class UserServerPaginationSpec
"return newest Users" in {
for {
// given
- _ <- usersReads.userReads.paginate(User.Sorting.NameAlphabetically, 0L, 1000).flatMap {
- case Paginated(entities, _) =>
+ _ <- usersReads.userReads
+ .paginate(User.Sorting.NameAlphabetically, Paginated.Offset(0L), Paginated.Limit(1000))
+ .flatMap { case Paginated(entities, _) =>
entities.traverse_(user => usersWrites.userWrites.deleteUser(User.Delete(user.id, None)))
- }
+ }
(CreationScheduled(userID), CreationScheduled(sessionID)) <- userCreate.flatMap(
usersWrites.userWrites.createUser
)
@@ -108,29 +106,29 @@ final class UserServerPaginationSpec
)
.eventually()
userIDs <- (0 until 9).toList
- .traverse(_ => userCreate.flatMap(usersWrites.userWrites.createUser).map(_._1.id))
+ .traverse(_ => userCreate.flatMap(usersWrites.userWrites.createUser).map(_._1.unwrap))
.map(_ :+ userID)
users <- userIDs.traverse(usersReads.userReads.requireById(_)).eventually()
// when
response1 <- UserAPIs.newest.toTestCall.untupled(
Authentication.Session(sessionID = sessionIDApi2Users.reverseGet(sessionID)),
None,
- PaginationLimit(5).some
+ Pagination.Limit(5).some
)
response2 <- UserAPIs.newest.toTestCall.untupled(
Authentication.Session(sessionID = sessionIDApi2Users.reverseGet(sessionID)),
- PaginationOffset(5L).some,
- PaginationLimit(5).some
+ Pagination.Offset(5L).some,
+ Pagination.Limit(5).some
)
} yield {
// then
- response1.code must_=== StatusCode.Ok
- response1.body must beValid(beRight(anInstanceOf[Pagination[APIUser]]))
- response2.code must_=== StatusCode.Ok
- response2.body must beValid(beRight(anInstanceOf[Pagination[APIUser]]))
+ response1.code === StatusCode.Ok
+ response1.body must beValid(beRight(beAnInstanceOf[Pagination[APIUser]]))
+ response2.code === StatusCode.Ok
+ response2.body must beValid(beRight(beAnInstanceOf[Pagination[APIUser]]))
(response1.body.toValidOpt.flatMap(_.toOption), response2.body.toValidOpt.flatMap(_.toOption))
.mapN { (pagination1, pagination2) =>
- (pagination1.entities.toSet ++ pagination2.entities.toSet) must_=== users.map(APIUser.fromDomain).toSet
+ (pagination1.entities.toSet ++ pagination2.entities.toSet) === users.map(APIUser.fromDomain).toSet
}
.getOrElse(pass)
}
diff --git a/modules/server/src/it/scala/io/branchtalk/users/api/UserServerSpec.scala b/modules/server/src/test/scala/io/branchtalk/users/api/UserServerSpec.scala
similarity index 70%
rename from modules/server/src/it/scala/io/branchtalk/users/api/UserServerSpec.scala
rename to modules/server/src/test/scala/io/branchtalk/users/api/UserServerSpec.scala
index f5e585b2..37b5bbc3 100644
--- a/modules/server/src/it/scala/io/branchtalk/users/api/UserServerSpec.scala
+++ b/modules/server/src/test/scala/io/branchtalk/users/api/UserServerSpec.scala
@@ -1,21 +1,22 @@
package io.branchtalk.users.api
import cats.effect.IO
-import io.branchtalk.api.{ Permission => _, RequiredPermissions => _, _ }
-import io.branchtalk.api.TapirSupport._
+import com.softwaremill.quicklens.*
+import io.branchtalk.api.{ Permission => _, RequiredPermissions => _, * }
+import io.branchtalk.api.TapirSupport.*
import io.branchtalk.discussions.DiscussionsFixtures
-import io.branchtalk.mappings._
-import io.branchtalk.shared.model._
+import io.branchtalk.mappings.*
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.infrastructure.*
import io.branchtalk.users.UsersFixtures
-import io.branchtalk.users.api.UserModels._
+import io.branchtalk.users.api.UserModels.*
import io.branchtalk.users.model.{ Password, Session, User }
-import monocle.macros.syntax.lens._
import org.specs2.mutable.Specification
import sttp.model.StatusCode
-final class UserServerSpec extends Specification with ServerIOTest with UsersFixtures with DiscussionsFixtures {
+final class UserServerSpec extends Specification, ServerIOTest, UsersFixtures, DiscussionsFixtures {
- implicit protected val uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
+ protected given uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
"UserServer-provided endpoints" should {
@@ -26,10 +27,11 @@ final class UserServerSpec extends Specification with ServerIOTest with UsersFix
"return newest Sessions" in {
for {
// given
- _ <- usersReads.userReads.paginate(User.Sorting.NameAlphabetically, 0L, 1000).flatMap {
- case Paginated(entities, _) =>
+ _ <- usersReads.userReads
+ .paginate(User.Sorting.NameAlphabetically, Paginated.Offset(0L), Paginated.Limit(1000))
+ .flatMap { case Paginated(entities, _) =>
entities.traverse_(user => usersWrites.userWrites.deleteUser(User.Delete(user.id, None)))
- }
+ }
(CreationScheduled(userID), CreationScheduled(sessionID)) <- userCreate.flatMap(
usersWrites.userWrites.createUser
)
@@ -42,24 +44,22 @@ final class UserServerSpec extends Specification with ServerIOTest with UsersFix
response1 <- UserAPIs.sessions.toTestCall.untupled(
Authentication.Session(sessionID = sessionIDApi2Users.reverseGet(sessionID)),
None,
- PaginationLimit(5).some
+ Pagination.Limit(5).some
)
response2 <- UserAPIs.sessions.toTestCall.untupled(
Authentication.Session(sessionID = sessionIDApi2Users.reverseGet(sessionID)),
- PaginationOffset(5L).some,
- PaginationLimit(5).some
+ Pagination.Offset(5L).some,
+ Pagination.Limit(5).some
)
} yield {
// then
- response1.code must_=== StatusCode.Ok
- response1.body must beValid(beRight(anInstanceOf[Pagination[APISession]]))
- response2.code must_=== StatusCode.Ok
- response2.body must beValid(beRight(anInstanceOf[Pagination[APISession]]))
+ response1.code === StatusCode.Ok
+ response1.body must beValid(beRight(beAnInstanceOf[Pagination[APISession]]))
+ response2.code === StatusCode.Ok
+ response2.body must beValid(beRight(beAnInstanceOf[Pagination[APISession]]))
(response1.body.toValidOpt.flatMap(_.toOption), response2.body.toValidOpt.flatMap(_.toOption))
.mapN { (pagination1, pagination2) =>
- (pagination1.entities.toSet ++ pagination2.entities.toSet) must_=== sessions
- .map(APISession.fromDomain)
- .toSet
+ (pagination1.entities.toSet ++ pagination2.entities.toSet) === sessions.map(APISession.fromDomain).toSet
}
.getOrElse(pass)
}
@@ -71,7 +71,7 @@ final class UserServerSpec extends Specification with ServerIOTest with UsersFix
"schedule User and Session creation on valid JSON" in {
for {
// given
- password <- Password.Raw.parse[IO]("password".getBytes)
+ password <- ParseNewtype[IO].parse[Password.Raw]("password".getBytes)
creationData <- userCreate
// when
response <- UserAPIs.signUp.toTestCall(
@@ -87,12 +87,12 @@ final class UserServerSpec extends Specification with ServerIOTest with UsersFix
session <- possibleResult.map(_.sessionID).traverse(usersReads.sessionReads.requireById).eventually()
} yield {
// then
- response.code must_=== StatusCode.Ok
- response.body must beValid(beRight(anInstanceOf[SignUpResponse]))
- user must beSome(anInstanceOf[User])
- session must beSome(anInstanceOf[Session])
+ response.code === StatusCode.Ok
+ response.body must beValid(beRight(beAnInstanceOf[SignUpResponse]))
+ user must beSome(beAnInstanceOf[User])
+ session must beSome(beAnInstanceOf[Session])
(user, session, response.body.toOption.flatMap(_.toOption))
- .mapN((u, s, r) => r must_=== SignUpResponse(u.id, s.id))
+ .mapN((u, s, r) => r === SignUpResponse(u.id, s.id))
.getOrElse(pass)
}
}
@@ -103,7 +103,7 @@ final class UserServerSpec extends Specification with ServerIOTest with UsersFix
"log User in on valid credentials" in {
for {
// given
- password <- Password.Raw.parse[IO]("password".getBytes)
+ password <- ParseNewtype[IO].parse[Password.Raw]("password".getBytes)
(CreationScheduled(userID), CreationScheduled(sessionID)) <- userCreate
.map(_.copy(password = Password.create(password)))
.flatMap(usersWrites.userWrites.createUser)
@@ -121,10 +121,10 @@ final class UserServerSpec extends Specification with ServerIOTest with UsersFix
)
} yield {
// then
- sessionResponse.code must_=== StatusCode.Ok
- sessionResponse.body must beValid(beRight(anInstanceOf[SignInResponse]))
- credentialsResponse.code must_=== StatusCode.Ok
- credentialsResponse.body must beValid(beRight(anInstanceOf[SignInResponse]))
+ sessionResponse.code === StatusCode.Ok
+ sessionResponse.body must beValid(beRight(beAnInstanceOf[SignInResponse]))
+ credentialsResponse.code === StatusCode.Ok
+ credentialsResponse.body must beValid(beRight(beAnInstanceOf[SignInResponse]))
}
}
}
@@ -134,7 +134,7 @@ final class UserServerSpec extends Specification with ServerIOTest with UsersFix
"log out User" in {
for {
// given
- password <- Password.Raw.parse[IO]("password".getBytes)
+ password <- ParseNewtype[IO].parse[Password.Raw]("password".getBytes)
(CreationScheduled(userID), CreationScheduled(sessionID)) <- userCreate
.map(_.copy(password = Password.create(password)))
.flatMap(usersWrites.userWrites.createUser)
@@ -152,10 +152,10 @@ final class UserServerSpec extends Specification with ServerIOTest with UsersFix
)
} yield {
// then
- sessionResponse.code must_=== StatusCode.Ok
- sessionResponse.body must beValid(beRight(be_===(SignOutResponse(userID, sessionID.some))))
- credentialsResponse.code must_=== StatusCode.Ok
- credentialsResponse.body must beValid(beRight(be_===(SignOutResponse(userID, None))))
+ sessionResponse.code === StatusCode.Ok
+ sessionResponse.body must beValid(beRight(be_==(SignOutResponse(userID, sessionID.some))))
+ credentialsResponse.code === StatusCode.Ok
+ credentialsResponse.body must beValid(beRight(be_==(SignOutResponse(userID, None))))
}
}
}
@@ -174,8 +174,8 @@ final class UserServerSpec extends Specification with ServerIOTest with UsersFix
response <- UserAPIs.fetchProfile.toTestCall.untupled(None, userID)
} yield {
// then
- response.code must_=== StatusCode.Ok
- response.body must beValid(beRight(be_===(APIUser.fromDomain(user))))
+ response.code === StatusCode.Ok
+ response.body must beValid(beRight(be_==(APIUser.fromDomain(user))))
}
}
}
@@ -190,9 +190,9 @@ final class UserServerSpec extends Specification with ServerIOTest with UsersFix
)
user <- usersReads.userReads.requireById(userID).eventually()
_ <- usersReads.sessionReads.requireById(sessionID).eventually()
- newUsername <- User.Name.parse[IO]("new test name")
+ newUsername <- ParseNewtype[IO].parse[User.Name]("new test name")
newDescription = User.Description("new test description")
- newPassword <- Password.Raw.parse[IO]("new password".getBytes)
+ newPassword <- ParseNewtype[IO].parse[Password.Raw]("new password".getBytes)
// when
response <- UserAPIs.updateProfile.toTestCall.untupled(
Authentication.Session(sessionID = sessionIDApi2Users.reverseGet(sessionID)),
@@ -209,17 +209,17 @@ final class UserServerSpec extends Specification with ServerIOTest with UsersFix
.eventually()
} yield {
// then
- response.code must_=== StatusCode.Ok
- response.body must beValid(beRight(be_===(UpdateUserResponse(userID))))
- updatedUser must_=== user
- .focus(_.data.username)
- .replace(newUsername)
- .focus(_.data.description)
- .replace(newDescription.some)
- .focus(_.data.password)
- .replace(updatedUser.data.password) // updated Password might have a different salt...
- .focus(_.data.lastModifiedAt)
- .replace(updatedUser.data.lastModifiedAt)
+ response.code === StatusCode.Ok
+ response.body must beValid(beRight(be_==(UpdateUserResponse(userID))))
+ updatedUser === user
+ .modify(_.data.username)
+ .setTo(newUsername)
+ .modify(_.data.description)
+ .setTo(newDescription.some)
+ .modify(_.data.password)
+ .setTo(updatedUser.data.password) // updated Password might have a different salt...
+ .modify(_.data.lastModifiedAt)
+ .setTo(updatedUser.data.lastModifiedAt)
updatedUser.data.password.verify(newPassword) must beTrue // ...which is why we test it separately
}
}
@@ -243,8 +243,8 @@ final class UserServerSpec extends Specification with ServerIOTest with UsersFix
_ <- usersReads.userReads.deleted(userID).assert("User should be eventually deleted")(identity).eventually()
} yield {
// then
- response.code must_=== StatusCode.Ok
- response.body must beValid(beRight(be_===(DeleteUserResponse(userID))))
+ response.code === StatusCode.Ok
+ response.body must beValid(beRight(be_==(DeleteUserResponse(userID))))
}
}
}
diff --git a/modules/users-api/src/main/scala/io/branchtalk/users/api/ChannelBanAPIs.scala b/modules/users-api/src/main/scala/io/branchtalk/users/api/ChannelBanAPIs.scala
index 7135daa1..061a58e4 100644
--- a/modules/users-api/src/main/scala/io/branchtalk/users/api/ChannelBanAPIs.scala
+++ b/modules/users-api/src/main/scala/io/branchtalk/users/api/ChannelBanAPIs.scala
@@ -1,10 +1,10 @@
package io.branchtalk.users.api
-import io.branchtalk.api._
-import io.branchtalk.api.AuthenticationSupport._
-import io.branchtalk.api.TapirSupport._
+import io.branchtalk.api.*
+import io.branchtalk.api.AuthenticationSupport.{ *, given }
+import io.branchtalk.api.TapirSupport.{ *, given }
import io.branchtalk.shared.model.ID
-import io.branchtalk.users.api.UserModels._
+import io.branchtalk.users.api.UserModels.*
import io.branchtalk.users.model.Channel
import sttp.model.StatusCode
@@ -31,7 +31,7 @@ object ChannelBanAPIs {
.out(jsonBody[BansResponse])
.errorOut(errorMapping)
.requiringPermissions(channelID =>
- RequiredPermissions.anyOf(Permission.ModerateChannel(ChannelID(channelID.uuid)), Permission.Administrate)
+ RequiredPermissions.anyOf(Permission.ModerateChannel(ChannelID(channelID.unwrap)), Permission.Administrate)
)
val orderBan: AuthedEndpoint[Authentication, (ID[Channel], BanOrderRequest), UserError, BanOrderResponse, Any] =
@@ -47,7 +47,7 @@ object ChannelBanAPIs {
.out(jsonBody[BanOrderResponse])
.errorOut(errorMapping)
.requiringPermissions { case (channelID, _) =>
- RequiredPermissions.anyOf(Permission.ModerateChannel(ChannelID(channelID.uuid)), Permission.Administrate)
+ RequiredPermissions.anyOf(Permission.ModerateChannel(ChannelID(channelID.unwrap)), Permission.Administrate)
}
val liftBan: AuthedEndpoint[Authentication, (ID[Channel], BanLiftRequest), UserError, BanLiftResponse, Any] =
@@ -63,6 +63,6 @@ object ChannelBanAPIs {
.out(jsonBody[BanLiftResponse])
.errorOut(errorMapping)
.requiringPermissions { case (channelID, _) =>
- RequiredPermissions.anyOf(Permission.ModerateChannel(ChannelID(channelID.uuid)), Permission.Administrate)
+ RequiredPermissions.anyOf(Permission.ModerateChannel(ChannelID(channelID.unwrap)), Permission.Administrate)
}
}
diff --git a/modules/users-api/src/main/scala/io/branchtalk/users/api/ChannelModerationAPIs.scala b/modules/users-api/src/main/scala/io/branchtalk/users/api/ChannelModerationAPIs.scala
index fd9146ba..b2310509 100644
--- a/modules/users-api/src/main/scala/io/branchtalk/users/api/ChannelModerationAPIs.scala
+++ b/modules/users-api/src/main/scala/io/branchtalk/users/api/ChannelModerationAPIs.scala
@@ -1,10 +1,10 @@
package io.branchtalk.users.api
-import io.branchtalk.api._
-import io.branchtalk.api.AuthenticationSupport._
-import io.branchtalk.api.TapirSupport._
+import io.branchtalk.api.*
+import io.branchtalk.api.AuthenticationSupport.{ *, given }
+import io.branchtalk.api.TapirSupport.{ *, given }
import io.branchtalk.shared.model.ID
-import io.branchtalk.users.api.UserModels._
+import io.branchtalk.users.api.UserModels.*
import io.branchtalk.users.model.Channel
import sttp.model.StatusCode
@@ -21,7 +21,7 @@ object ChannelModerationAPIs {
val paginate: AuthedEndpoint[
Authentication,
- (ID[Channel], Option[PaginationOffset], Option[PaginationLimit]),
+ (ID[Channel], Option[Pagination.Offset], Option[Pagination.Limit]),
UserError,
Pagination[APIUser],
Any
@@ -33,12 +33,12 @@ object ChannelModerationAPIs {
.get
.securityIn(authHeader)
.in(prefix)
- .in(query[Option[PaginationOffset]]("offset"))
- .in(query[Option[PaginationLimit]]("limit"))
+ .in(query[Option[Pagination.Offset]]("offset"))
+ .in(query[Option[Pagination.Limit]]("limit"))
.out(jsonBody[Pagination[APIUser]])
.errorOut(errorMapping)
.requiringPermissions { case (channelID, _, _) =>
- RequiredPermissions.anyOf(Permission.ModerateChannel(ChannelID(channelID.uuid)), Permission.Administrate)
+ RequiredPermissions.anyOf(Permission.ModerateChannel(ChannelID(channelID.unwrap)), Permission.Administrate)
}
val grantChannelModeration: AuthedEndpoint[
@@ -59,7 +59,7 @@ object ChannelModerationAPIs {
.out(jsonBody[GrantModerationResponse])
.errorOut(errorMapping)
.requiringPermissions { case (channelID, _) =>
- RequiredPermissions.anyOf(Permission.ModerateChannel(ChannelID(channelID.uuid)), Permission.Administrate)
+ RequiredPermissions.anyOf(Permission.ModerateChannel(ChannelID(channelID.unwrap)), Permission.Administrate)
}
val revokeChannelModeration: AuthedEndpoint[
@@ -80,6 +80,6 @@ object ChannelModerationAPIs {
.out(jsonBody[RevokeModerationResponse])
.errorOut(errorMapping)
.requiringPermissions { case (channelID, _) =>
- RequiredPermissions.anyOf(Permission.ModerateChannel(ChannelID(channelID.uuid)), Permission.Administrate)
+ RequiredPermissions.anyOf(Permission.ModerateChannel(ChannelID(channelID.unwrap)), Permission.Administrate)
}
}
diff --git a/modules/users-api/src/main/scala/io/branchtalk/users/api/UserAPIs.scala b/modules/users-api/src/main/scala/io/branchtalk/users/api/UserAPIs.scala
index 98ce8413..198c4125 100644
--- a/modules/users-api/src/main/scala/io/branchtalk/users/api/UserAPIs.scala
+++ b/modules/users-api/src/main/scala/io/branchtalk/users/api/UserAPIs.scala
@@ -1,10 +1,10 @@
package io.branchtalk.users.api
-import io.branchtalk.api._
-import io.branchtalk.api.AuthenticationSupport._
-import io.branchtalk.api.TapirSupport._
+import io.branchtalk.api.*
+import io.branchtalk.api.AuthenticationSupport.{ *, given }
+import io.branchtalk.api.TapirSupport.{ *, given }
import io.branchtalk.shared.model.{ ID, OptionUpdatable, Updatable }
-import io.branchtalk.users.api.UserModels._
+import io.branchtalk.users.api.UserModels.*
import io.branchtalk.users.model.Password.{ Raw => RawPassword }
import io.branchtalk.users.model.User
import sttp.model.StatusCode
@@ -22,7 +22,7 @@ object UserAPIs {
val paginate: AuthedEndpoint[
Authentication,
- (Option[PaginationOffset], Option[PaginationLimit]),
+ (Option[Pagination.Offset], Option[Pagination.Limit]),
UserError,
Pagination[APIUser],
Any
@@ -34,15 +34,15 @@ object UserAPIs {
.get
.securityIn(authHeader)
.in(prefix)
- .in(query[Option[PaginationOffset]]("offset"))
- .in(query[Option[PaginationLimit]]("limit"))
+ .in(query[Option[Pagination.Offset]]("offset"))
+ .in(query[Option[Pagination.Limit]]("limit"))
.out(jsonBody[Pagination[APIUser]])
.errorOut(errorMapping)
.requiringPermissions(_ => RequiredPermissions.one(Permission.ModerateUsers))
val newest: AuthedEndpoint[
Authentication,
- (Option[PaginationOffset], Option[PaginationLimit]),
+ (Option[Pagination.Offset], Option[Pagination.Limit]),
UserError,
Pagination[APIUser],
Any
@@ -54,15 +54,15 @@ object UserAPIs {
.get
.securityIn(authHeader)
.in(prefix / "newest")
- .in(query[Option[PaginationOffset]]("offset"))
- .in(query[Option[PaginationLimit]]("limit"))
+ .in(query[Option[Pagination.Offset]]("offset"))
+ .in(query[Option[Pagination.Limit]]("limit"))
.out(jsonBody[Pagination[APIUser]])
.errorOut(errorMapping)
.requiringPermissions(_ => RequiredPermissions.one(Permission.ModerateUsers))
val sessions: AuthedEndpoint[
Authentication,
- (Option[PaginationOffset], Option[PaginationLimit]),
+ (Option[Pagination.Offset], Option[Pagination.Limit]),
UserError,
Pagination[APISession],
Any
@@ -74,8 +74,8 @@ object UserAPIs {
.get
.securityIn(authHeader)
.in(prefix / "sessions")
- .in(query[Option[PaginationOffset]]("offset"))
- .in(query[Option[PaginationLimit]]("limit"))
+ .in(query[Option[Pagination.Offset]]("offset"))
+ .in(query[Option[Pagination.Limit]]("limit"))
.out(jsonBody[Pagination[APISession]])
.errorOut(errorMapping)
.notRequiringPermissions
@@ -143,7 +143,7 @@ object UserAPIs {
UpdateUserRequest(
newUsername = Updatable.Set(User.Name("example")),
newDescription = OptionUpdatable.Set(User.Description("example")),
- newPassword = Updatable.Set(RawPassword.fromString("example"))
+ newPassword = Updatable.Set(RawPassword.unsafeMake("example".getBytes))
),
name = "Set all".some,
summary = "Assigns new value to all fields".some
diff --git a/modules/users-api/src/main/scala/io/branchtalk/users/api/UserBanAPIs.scala b/modules/users-api/src/main/scala/io/branchtalk/users/api/UserBanAPIs.scala
index 94d9bdf9..34c66aed 100644
--- a/modules/users-api/src/main/scala/io/branchtalk/users/api/UserBanAPIs.scala
+++ b/modules/users-api/src/main/scala/io/branchtalk/users/api/UserBanAPIs.scala
@@ -1,9 +1,9 @@
package io.branchtalk.users.api
-import io.branchtalk.api._
-import io.branchtalk.api.AuthenticationSupport._
-import io.branchtalk.api.TapirSupport._
-import io.branchtalk.users.api.UserModels._
+import io.branchtalk.api.*
+import io.branchtalk.api.AuthenticationSupport.{ *, given }
+import io.branchtalk.api.TapirSupport.{ *, given }
+import io.branchtalk.users.api.UserModels.*
import sttp.model.StatusCode
object UserBanAPIs {
diff --git a/modules/users-api/src/main/scala/io/branchtalk/users/api/UserModels.scala b/modules/users-api/src/main/scala/io/branchtalk/users/api/UserModels.scala
index 099d0a0c..131b6bf6 100644
--- a/modules/users-api/src/main/scala/io/branchtalk/users/api/UserModels.scala
+++ b/modules/users-api/src/main/scala/io/branchtalk/users/api/UserModels.scala
@@ -2,82 +2,54 @@ package io.branchtalk.users.api
import java.time.OffsetDateTime
import cats.data.NonEmptyList
-import com.github.plokhotnyuk.jsoniter_scala.macros._
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.collection.NonEmpty
-import eu.timepit.refined.refineV
-import eu.timepit.refined.string.MatchesRegex
-import eu.timepit.refined.types.string.NonEmptyString
-import io.branchtalk.ADT
-import io.branchtalk.api.JsoniterSupport._
-import io.branchtalk.api.TapirSupport._
-import io.branchtalk.shared.model.{ ID, OptionUpdatable, Updatable }
-import io.branchtalk.users.model.SessionProperties.Usage.Type
-import io.branchtalk.users.model._
-import io.scalaland.catnip.Semi
-import io.scalaland.chimney.dsl._
+import com.github.plokhotnyuk.jsoniter_scala.macros.*
+import io.branchtalk.api.JsoniterSupport.{ *, given }
+import io.branchtalk.api.TapirSupport.{ *, given }
+import io.branchtalk.shared.model.*
+import io.branchtalk.users.model.*
+import io.scalaland.chimney.dsl.*
import sttp.tapir.Schema
@SuppressWarnings(Array("org.wartremover.warts.All")) // for macros
object UserModels {
// properties codecs
- implicit val userEmailCodec: JsCodec[User.Email] =
- summonCodec[String](JsonCodecMaker.make).refine[MatchesRegex["(.+)@(.+)"]].asNewtype[User.Email]
- implicit val usernameCodec: JsCodec[User.Name] =
- summonCodec[String](JsonCodecMaker.make).refine[NonEmpty].asNewtype[User.Name]
- implicit val userDescriptionCodec: JsCodec[User.Description] =
- summonCodec[String](JsonCodecMaker.make).asNewtype[User.Description]
- implicit val passwordRawCodec: JsCodec[Password.Raw] =
- summonCodec[String](JsonCodecMaker.make)
- .map[Array[Byte]](_.getBytes)(new String(_)) // I wanted to avoid that but the result is ugly :/
- .refine[NonEmpty] // I'll try to revisit that someday and e.g. use Base64 here?
- .asNewtype[Password.Raw]
- implicit val permissionsCodec: JsCodec[Permissions] =
- summonCodec[Set[Permission]](JsonCodecMaker.make).asNewtype[Permissions]
- implicit val sessionExpirationCodec: JsCodec[Session.ExpirationTime] =
- summonCodec[OffsetDateTime](JsonCodecMaker.make).asNewtype[Session.ExpirationTime]
- implicit val banReasonCodec: JsCodec[Ban.Reason] =
- summonCodec[String](JsonCodecMaker.make).refine[NonEmpty].asNewtype[Ban.Reason]
+ given JsCodec[User.Email] = newtypeCodec
+ given JsCodec[User.Name] = newtypeCodec
+ given JsCodec[User.Description] = newtypeCodec
+ // I wanted to avoid that but the result is ugly :/
+ // I'll try to revisit that someday and e.g. use Base64 here?
+ given JsCodec[Password.Raw] =
+ DefaultJsCodec.derived[String].map[Array[Byte]](_.getBytes)(new String(_)).asNewtypeCodec
+ given JsCodec[Permissions] = newtypeCodec
+ given JsCodec[Session.ExpirationTime] = newtypeCodec
+ given JsCodec[Ban.Reason] = newtypeCodec
// properties schemas
- implicit val userEmailSchema: JsSchema[User.Email] =
- summonSchema[String Refined MatchesRegex["(.+)@(.+)"]].asNewtype[User.Email]
- implicit val usernameSchema: JsSchema[User.Name] =
- summonSchema[String Refined NonEmpty].asNewtype[User.Name]
- implicit val userDescriptionSchema: JsSchema[User.Description] =
- summonSchema[String].asNewtype[User.Description]
- implicit val passwordRawSchema: JsSchema[Password.Raw] =
- summonSchema[String]
- .map[Array[Byte] Refined NonEmpty](_.getBytes.pipe(refineV[NonEmpty](_).toOption))(_.value.pipe(new String(_)))
- .asNewtype[Password.Raw]
- implicit val permissionSchema: JsSchema[Permission] =
- Schema.derived[Permission]
- implicit val permissionsSchema: JsSchema[Permissions] =
- summonSchema[Set[Permission]].asNewtype[Permissions]
- implicit val sessionExpirationSchema: JsSchema[Session.ExpirationTime] =
- summonSchema[OffsetDateTime].asNewtype[Session.ExpirationTime]
- implicit val banReasonSchema: JsSchema[Ban.Reason] =
- summonSchema[NonEmptyString].asNewtype[Ban.Reason]
-
- @Semi(JsCodec, JsSchema) sealed trait UserError extends ADT
+ given JsSchema[Permission] = JsSchema.derived
+ given JsSchema[Permissions] = summonSchema[List[Permission]].as[Set[Permission]].asNewtypeSchema[Permissions]
+ given JsSchema[Password.Raw] =
+ summonSchema[String].map[Array[Byte]](_.getBytes.some)(new String(_)).asNewtypeSchema[Password.Raw]
+
+ sealed trait UserError derives DefaultJsCodec, JsSchema
object UserError {
- @Semi(JsCodec, JsSchema) final case class BadCredentials(msg: String) extends UserError
- @Semi(JsCodec, JsSchema) final case class NoPermission(msg: String) extends UserError
- @Semi(JsCodec, JsSchema) final case class NotFound(msg: String) extends UserError
- @Semi(JsCodec, JsSchema) final case class ValidationFailed(error: NonEmptyList[String]) extends UserError
+ final case class BadCredentials(msg: String) extends UserError derives DefaultJsCodec, JsSchema
+ final case class NoPermission(msg: String) extends UserError derives DefaultJsCodec, JsSchema
+ final case class NotFound(msg: String) extends UserError derives DefaultJsCodec, JsSchema
+ final case class ValidationFailed(error: NonEmptyList[String]) extends UserError derives DefaultJsCodec, JsSchema
}
- @Semi(JsCodec, JsSchema) final case class APISession(
+ final case class APISession(
id: ID[Session],
userID: ID[User],
sessionType: APISession.SessionType,
expiresAt: Session.ExpirationTime
- )
+ ) derives DefaultJsCodec,
+ JsSchema
object APISession {
- @Semi(JsCodec, JsSchema) sealed trait SessionType extends ADT
+ sealed trait SessionType derives DefaultJsCodec, JsSchema
object SessionType {
case object UserSession extends SessionType
case object OAuth extends SessionType
@@ -86,8 +58,8 @@ object UserModels {
def fromDomain(session: Session): APISession = {
val Session.Usage.Tupled(domainSessionType, _) = session.data.usage
val sessionType = domainSessionType match {
- case Type.UserSession => SessionType.UserSession
- case Type.OAuth => SessionType.OAuth
+ case Session.Usage.Type.UserSession => SessionType.UserSession
+ case Session.Usage.Type.OAuth => SessionType.OAuth
}
session.data
.into[APISession]
@@ -97,64 +69,66 @@ object UserModels {
}
}
- @Semi(JsCodec, JsSchema) final case class SignUpRequest(
+ final case class SignUpRequest(
email: User.Email,
username: User.Name,
description: Option[User.Description],
password: Password.Raw
- )
- @Semi(JsCodec, JsSchema) final case class SignUpResponse(
+ ) derives DefaultJsCodec,
+ JsSchema
+ final case class SignUpResponse(
userID: ID[User],
sessionID: ID[Session]
- )
+ ) derives DefaultJsCodec,
+ JsSchema
- @Semi(JsCodec, JsSchema) final case class SignInResponse(
+ final case class SignInResponse(
userID: ID[User],
sessionID: ID[Session],
expiresAt: Session.ExpirationTime
- )
+ ) derives DefaultJsCodec,
+ JsSchema
- @Semi(JsCodec, JsSchema) final case class SignOutResponse(
+ final case class SignOutResponse(
userID: ID[User],
sessionID: Option[ID[Session]] // in case user wasn't using sessionID
- )
+ ) derives DefaultJsCodec,
+ JsSchema
- @Semi(JsCodec, JsSchema) final case class APIUser(
+ final case class APIUser(
id: ID[User],
email: User.Email,
username: User.Name,
description: Option[User.Description],
permissions: Permissions
- )
+ ) derives DefaultJsCodec,
+ JsSchema
object APIUser {
def fromDomain(user: User): APIUser = user.data.into[APIUser].withFieldConst(_.id, user.id).transform
}
- @Semi(JsCodec, JsSchema) final case class UpdateUserRequest(
+ final case class UpdateUserRequest(
newUsername: Updatable[User.Name],
newDescription: OptionUpdatable[User.Description],
newPassword: Updatable[Password.Raw]
- )
- @Semi(JsCodec, JsSchema) final case class UpdateUserResponse(id: ID[User])
-
- @Semi(JsCodec, JsSchema) final case class DeleteUserResponse(id: ID[User])
-
- @Semi(JsCodec, JsSchema) final case class GrantModerationRequest(id: ID[User])
-
- @Semi(JsCodec, JsSchema) final case class GrantModerationResponse(id: ID[User])
-
- @Semi(JsCodec, JsSchema) final case class RevokeModerationRequest(id: ID[User])
+ ) derives DefaultJsCodec,
+ JsSchema
+ final case class UpdateUserResponse(id: ID[User]) derives DefaultJsCodec, JsSchema
- @Semi(JsCodec, JsSchema) final case class RevokeModerationResponse(id: ID[User])
+ final case class DeleteUserResponse(id: ID[User]) derives DefaultJsCodec, JsSchema
- @Semi(JsCodec, JsSchema) final case class BansResponse(bannedIDs: List[ID[User]])
+ final case class GrantModerationRequest(id: ID[User]) derives DefaultJsCodec, JsSchema
+ final case class GrantModerationResponse(id: ID[User]) derives DefaultJsCodec, JsSchema
- @Semi(JsCodec, JsSchema) final case class BanOrderRequest(id: ID[User], reason: Ban.Reason)
+ final case class RevokeModerationRequest(id: ID[User]) derives DefaultJsCodec, JsSchema
+ final case class RevokeModerationResponse(id: ID[User]) derives DefaultJsCodec, JsSchema
- @Semi(JsCodec, JsSchema) final case class BanOrderResponse(id: ID[User])
+ final case class BansResponse(bannedIDs: List[ID[User]]) derives DefaultJsCodec, JsSchema
- @Semi(JsCodec, JsSchema) final case class BanLiftRequest(id: ID[User])
+ final case class BanOrderRequest(id: ID[User], reason: Ban.Reason) derives DefaultJsCodec, JsSchema
+ final case class BanOrderResponse(id: ID[User]) derives DefaultJsCodec, JsSchema
- @Semi(JsCodec, JsSchema) final case class BanLiftResponse(id: ID[User])
+ final case class BanLiftRequest(id: ID[User]) derives DefaultJsCodec, JsSchema
+ final case class BanLiftResponse(id: ID[User]) derives DefaultJsCodec, JsSchema
}
diff --git a/modules/users-api/src/main/scala/io/branchtalk/users/api/UserModerationAPIs.scala b/modules/users-api/src/main/scala/io/branchtalk/users/api/UserModerationAPIs.scala
index 0bc26bef..a5a2a5f2 100644
--- a/modules/users-api/src/main/scala/io/branchtalk/users/api/UserModerationAPIs.scala
+++ b/modules/users-api/src/main/scala/io/branchtalk/users/api/UserModerationAPIs.scala
@@ -1,9 +1,9 @@
package io.branchtalk.users.api
-import io.branchtalk.api._
-import io.branchtalk.api.AuthenticationSupport._
-import io.branchtalk.api.TapirSupport._
-import io.branchtalk.users.api.UserModels._
+import io.branchtalk.api.*
+import io.branchtalk.api.AuthenticationSupport.{ *, given }
+import io.branchtalk.api.TapirSupport.{ *, given }
+import io.branchtalk.users.api.UserModels.{ *, given }
import sttp.model.StatusCode
object UserModerationAPIs {
@@ -19,7 +19,7 @@ object UserModerationAPIs {
val paginate: AuthedEndpoint[
Authentication,
- (Option[PaginationOffset], Option[PaginationLimit]),
+ (Option[Pagination.Offset], Option[Pagination.Limit]),
UserError,
Pagination[APIUser],
Any
@@ -31,8 +31,8 @@ object UserModerationAPIs {
.get
.securityIn(authHeader)
.in(prefix)
- .in(query[Option[PaginationOffset]]("offset"))
- .in(query[Option[PaginationLimit]]("limit"))
+ .in(query[Option[Pagination.Offset]]("offset"))
+ .in(query[Option[Pagination.Limit]]("limit"))
.out(jsonBody[Pagination[APIUser]])
.errorOut(errorMapping)
.requiringPermissions(_ => RequiredPermissions.anyOf(Permission.ModerateUsers, Permission.Administrate))
diff --git a/modules/users-impl/src/main/scala/io/branchtalk/users/UsersModule.scala b/modules/users-impl/src/main/scala/io/branchtalk/users/UsersModule.scala
index 6aadc9b1..ded0ba5d 100644
--- a/modules/users-impl/src/main/scala/io/branchtalk/users/UsersModule.scala
+++ b/modules/users-impl/src/main/scala/io/branchtalk/users/UsersModule.scala
@@ -3,14 +3,13 @@ package io.branchtalk.users
import cats.data.NonEmptyList
import cats.effect.{ Async, Resource }
import cats.effect.std.Dispatcher
-import com.softwaremill.macwire.wire
import io.branchtalk.discussions.events.DiscussionEvent
-import io.branchtalk.logging.MDC
-import io.branchtalk.shared.infrastructure._
-import io.branchtalk.shared.model.{ Logger, UUID, UUIDGenerator }
+import io.branchtalk.logging.*
+import io.branchtalk.shared.infrastructure.*
+import io.branchtalk.shared.model.UUID
import io.branchtalk.users.events.{ UsersCommandEvent, UsersEvent }
-import io.branchtalk.users.reads._
-import io.branchtalk.users.writes._
+import io.branchtalk.users.reads.*
+import io.branchtalk.users.writes.*
import io.prometheus.client.CollectorRegistry
import scala.annotation.nowarn
@@ -28,7 +27,7 @@ final case class UsersWrites[F[_]](
runProjections: StreamRunner[F],
runDiscussionsConsumer: StreamRunner.FromConsumerStream[F, DiscussionEvent]
)
-@nowarn("cat=unused") // macwire
+
object UsersModule {
private val module = DomainModule[UsersEvent, UsersCommandEvent]
@@ -38,80 +37,78 @@ object UsersModule {
val discussionsProjectionName = "discussions"
def reads[F[_]: Async](
- domainConfig: DomainConfig,
+ domainConfig: DomainModule.Config,
registry: CollectorRegistry
): Resource[F, UsersReads[F]] =
- Logger.getLogger[F].pipe { logger =>
- Resource.make(logger.info("Initialize Users reads"))(_ => logger.info("Shut down Users reads"))
- } >>
- module.setupReads[F](domainConfig, registry).map { case ReadsInfrastructure(transactor, _) =>
- val userReads: UserReads[F] = wire[UserReadsImpl[F]]
- val sessionReads: SessionReads[F] = wire[SessionReadsImpl[F]]
- val banReads: BanReads[F] = wire[BanReadsImpl[F]]
+ for {
+ logger <- Resource.eval(Logger.create[F])
+ _ <- Resource.make(logger.info("Initialize Users reads"))(_ => logger.info("Shut down Users reads"))
+ case Reads.Infrastructure(transactor, _) <- module.setupReads[F](domainConfig, logger, registry)
+ } yield {
+ // macwire got removed due to:
+ // https://github.com/softwaremill/macwire/blob/abf95284e24138a984a06ef4f08d0788af371825/macros/src/main/scala-3/com/softwaremill/macwire/internals/ConstructorCrimper.scala#L91
+ val userReads: UserReads[F] = UserReadsImpl[F](transactor)
+ val sessionReads: SessionReads[F] = SessionReadsImpl[F](transactor)
+ val banReads: BanReads[F] = BanReadsImpl[F](transactor)
- wire[UsersReads[F]]
- }
+ UsersReads(userReads, sessionReads, banReads)
+ }
- // scalastyle:off method.length
def writes[F[_]: Async: Dispatcher: MDC](
- domainConfig: DomainConfig,
- registry: CollectorRegistry
- )(implicit uuidGenerator: UUIDGenerator): Resource[F, UsersWrites[F]] =
- Logger.getLogger[F].pipe { logger =>
- Resource.make(logger.info("Initialize Users writes"))(_ => logger.info("Shut down Users writes")) >>
- module.setupWrites[F](domainConfig, registry).map {
- case WritesInfrastructure(transactor,
- internalProducer,
- internalConsumerStream,
- producer,
- consumerStream,
- cache
- ) =>
- val userWrites: UserWrites[F] = wire[UserWritesImpl[F]]
- val sessionWrites: SessionWrites[F] = wire[SessionWritesImpl[F]]
- val banWrites: BanWrites[F] = wire[BanWritesImpl[F]]
+ domainConfig: DomainModule.Config,
+ registry: CollectorRegistry
+ )(using UUID.Generator): Resource[F, UsersWrites[F]] =
+ for {
+ logger <- Resource.eval(Logger.create[F])
+ _ <- Resource.make(logger.info("Initialize Users writes"))(_ => logger.info("Shut down Users writes"))
+ case Writes.Infrastructure(transactor,
+ internalProducer,
+ internalConsumerStream,
+ producer,
+ consumerStream,
+ cache
+ ) <- module.setupWrites[F](domainConfig, logger, registry)
+ } yield {
+ val userWrites: UserWrites[F] = UserWritesImpl[F](internalProducer, transactor)
+ val sessionWrites: SessionWrites[F] = SessionWritesImpl[F](producer, transactor)
+ val banWrites: BanWrites[F] = BanWritesImpl[F](internalProducer, transactor)
- val commandHandler: Projector[F, UsersCommandEvent, (UUID, UsersEvent)] = NonEmptyList
- .of(
- wire[UserCommandHandler[F]],
- wire[BanCommandHandler[F]]
- )
- .reduce
- val postgresProjector: Projector[F, UsersEvent, (UUID, UsersEvent)] = NonEmptyList
- .of(
- wire[UserPostgresProjector[F]],
- wire[BanPostgresProjector[F]]
- )
- .reduce
- val runProjections: StreamRunner[F] = {
- val runCommandProjector: StreamRunner[F] =
- internalConsumerStream.runCachedThrough(logger, cache)(
- ConsumerStream.noID.andThen(commandHandler).andThen(producer).andThen(ConsumerStream.produced)
- )
- val runPostgresProjector: StreamRunner[F] =
- consumerStream(domainConfig.consumers(postgresProjectionName)).runCachedThrough(logger, cache)(
- ConsumerStream.noID.andThen(postgresProjector).andThen(ConsumerStream.noID)
- )
- runCommandProjector |+| runPostgresProjector
- }
+ val commandHandler: Projector[F, UsersCommandEvent, (UUID, UsersEvent)] = NonEmptyList
+ .of(
+ UserCommandHandler[F],
+ BanCommandHandler[F]
+ )
+ .reduce
+ val postgresProjector: Projector[F, UsersEvent, (UUID, UsersEvent)] = NonEmptyList
+ .of(
+ UserPostgresProjector[F](transactor),
+ BanPostgresProjector[F](transactor)
+ )
+ .reduce
+ val runProjections: StreamRunner[F] = {
+ val runCommandProjector: StreamRunner[F] =
+ internalConsumerStream.runCachedThrough(logger, cache)(
+ ConsumerStream.noID.andThen(commandHandler).andThen(producer).andThen(ConsumerStream.produced)
+ )
+ val runPostgresProjector: StreamRunner[F] =
+ consumerStream(domainConfig.consumers(postgresProjectionName)).runCachedThrough(logger, cache)(
+ ConsumerStream.noID.andThen(postgresProjector).andThen(ConsumerStream.noID)
+ )
+ runCommandProjector |+| runPostgresProjector
+ }
- val discussionsConsumer: DiscussionsConsumer[F] = wire[DiscussionsConsumer[F]]
- val runDiscussionsConsumer: StreamRunner.FromConsumerStream[F, DiscussionEvent] =
- _.runThrough(logger)(
- ConsumerStream.noID
- .andThen(discussionsConsumer)
- .andThen(internalProducer)
- .andThen(ConsumerStream.produced)
- )
+ val discussionsConsumer: DiscussionsConsumer[F] = DiscussionsConsumer[F]
+ val runDiscussionsConsumer: StreamRunner.FromConsumerStream[F, DiscussionEvent] =
+ _.runThrough(logger)(
+ ConsumerStream.noID.andThen(discussionsConsumer).andThen(internalProducer).andThen(ConsumerStream.produced)
+ )
- wire[UsersWrites[F]]
- }
+ UsersWrites(userWrites, sessionWrites, banWrites, runProjections, runDiscussionsConsumer)
}
- // scalastyle:on method.length
- def listenToUsers[F[_]](domainConfig: DomainConfig)(
- discussionEventConsumer: ConsumerStream.Factory[F, DiscussionEvent],
- runDiscussionsConsumer: StreamRunner.FromConsumerStream[F, DiscussionEvent]
+ def listenToUsers[F[_]](domainConfig: DomainModule.Config)(
+ discussionEventConsumer: ConsumerStream.Factory[F, DiscussionEvent],
+ runDiscussionsConsumer: StreamRunner.FromConsumerStream[F, DiscussionEvent]
): StreamRunner[F] =
(discussionEventConsumer andThen runDiscussionsConsumer)(domainConfig.consumers(discussionsProjectionName))
}
diff --git a/modules/users-impl/src/main/scala/io/branchtalk/users/events/BanCommandEvent.scala b/modules/users-impl/src/main/scala/io/branchtalk/users/events/BanCommandEvent.scala
index f92f242d..7d422076 100644
--- a/modules/users-impl/src/main/scala/io/branchtalk/users/events/BanCommandEvent.scala
+++ b/modules/users-impl/src/main/scala/io/branchtalk/users/events/BanCommandEvent.scala
@@ -1,30 +1,28 @@
package io.branchtalk.users.events
-import com.sksamuel.avro4s._
-import io.branchtalk.ADT
+import com.sksamuel.avro4s.*
import io.branchtalk.logging.CorrelationID
-import io.branchtalk.shared.model._
-import io.branchtalk.shared.model.AvroSupport._
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.model.AvroSupport.{ *, given }
import io.branchtalk.users.model.{ Ban, User }
-import io.scalaland.catnip.Semi
-@Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) sealed trait BanCommandEvent extends ADT
+sealed trait BanCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
object BanCommandEvent {
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class OrderBan(
+ final case class OrderBan(
bannedUserID: ID[User],
moderatorID: Option[ID[User]],
reason: Ban.Reason,
scope: Ban.Scope,
createdAt: CreationTime,
correlationID: CorrelationID
- ) extends BanCommandEvent
+ ) extends BanCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class LiftBan(
+ final case class LiftBan(
bannedUserID: ID[User],
moderatorID: Option[ID[User]],
scope: Ban.Scope,
modifiedAt: ModificationTime,
correlationID: CorrelationID
- ) extends BanCommandEvent
+ ) extends BanCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
}
diff --git a/modules/users-impl/src/main/scala/io/branchtalk/users/events/UserCommandEvent.scala b/modules/users-impl/src/main/scala/io/branchtalk/users/events/UserCommandEvent.scala
index 1c1c0b02..9b04cd2c 100644
--- a/modules/users-impl/src/main/scala/io/branchtalk/users/events/UserCommandEvent.scala
+++ b/modules/users-impl/src/main/scala/io/branchtalk/users/events/UserCommandEvent.scala
@@ -1,19 +1,18 @@
package io.branchtalk.users.events
-import com.sksamuel.avro4s._
-import io.branchtalk.ADT
+import com.sksamuel.avro4s.*
import io.branchtalk.logging.CorrelationID
-import io.branchtalk.shared.model._
+import io.branchtalk.shared.model.*
import io.branchtalk.shared.model.AvroSerialization.DeserializationResult
-import io.branchtalk.shared.model.AvroSupport._
+import io.branchtalk.shared.model.AvroSupport.{ *, given }
import io.branchtalk.users.model.{ Password, Permission, Session, User }
-import io.scalaland.catnip.Semi
-import io.scalaland.chimney.dsl._
+import io.scalaland.chimney.dsl.*
+import io.scalaland.chimney.partial.syntax.*
-@Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) sealed trait UserCommandEvent extends ADT
+sealed trait UserCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
object UserCommandEvent {
- @Semi(FastEq, ShowPretty) final case class Create(
+ final case class Create(
id: ID[User],
email: SensitiveData[User.Email],
username: SensitiveData[User.Name],
@@ -23,7 +22,8 @@ object UserCommandEvent {
sessionID: ID[Session],
sessionExpiresAt: Session.ExpirationTime,
correlationID: CorrelationID
- ) {
+ ) derives FastEq,
+ ShowPretty {
def encrypt(
algorithm: SensitiveData.Algorithm,
@@ -37,7 +37,7 @@ object UserCommandEvent {
}
object Create {
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Encrypted(
+ final case class Encrypted(
id: ID[User],
email: SensitiveData.Encrypted[User.Email],
username: SensitiveData.Encrypted[User.Name],
@@ -47,21 +47,23 @@ object UserCommandEvent {
sessionID: ID[Session],
sessionExpiresAt: Session.ExpirationTime,
correlationID: CorrelationID
- ) extends UserCommandEvent {
+ ) extends UserCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor {
def decrypt(
algorithm: SensitiveData.Algorithm,
key: SensitiveData.Key
): DeserializationResult[Create] = this
- .intoF[DeserializationResult, Create]
- .withFieldComputedF(_.email, _.email.decrypt(algorithm, key))
- .withFieldComputedF(_.username, _.username.decrypt(algorithm, key))
- .withFieldComputedF(_.password, _.password.decrypt(algorithm, key))
+ .intoPartial[Create]
+ .withFieldComputedPartial(_.email, _.email.decrypt(algorithm, key).asResult)
+ .withFieldComputedPartial(_.username, _.username.decrypt(algorithm, key).asResult)
+ .withFieldComputedPartial(_.password, _.password.decrypt(algorithm, key).asResult)
.transform
+ .asEither
+ .leftMap(e => DeserializationError.DecryptionError(this.show, e.asErrorPathMessageStrings.toMap))
}
}
- @Semi(FastEq, ShowPretty) final case class Update(
+ final case class Update(
id: ID[User],
moderatorID: Option[ID[User]],
newUsername: Updatable[SensitiveData[User.Name]],
@@ -70,7 +72,8 @@ object UserCommandEvent {
updatePermissions: List[Permission.Update],
modifiedAt: ModificationTime,
correlationID: CorrelationID
- ) {
+ ) derives FastEq,
+ ShowPretty {
def encrypt(
algorithm: SensitiveData.Algorithm,
@@ -83,7 +86,7 @@ object UserCommandEvent {
}
object Update {
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Encrypted(
+ final case class Encrypted(
id: ID[User],
moderatorID: Option[ID[User]],
newUsername: Updatable[SensitiveData.Encrypted[User.Name]],
@@ -92,23 +95,25 @@ object UserCommandEvent {
updatePermissions: List[Permission.Update],
modifiedAt: ModificationTime,
correlationID: CorrelationID
- ) extends UserCommandEvent {
+ ) extends UserCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor {
def decrypt(
algorithm: SensitiveData.Algorithm,
key: SensitiveData.Key
): DeserializationResult[Update] = this
- .intoF[DeserializationResult, Update]
- .withFieldComputedF(_.newUsername, _.newUsername.traverse(_.decrypt(algorithm, key)))
- .withFieldComputedF(_.newPassword, _.newPassword.traverse(_.decrypt(algorithm, key)))
+ .intoPartial[Update]
+ .withFieldComputedPartial(_.newUsername, _.newUsername.traverse(_.decrypt(algorithm, key)).asResult)
+ .withFieldComputedPartial(_.newPassword, _.newPassword.traverse(_.decrypt(algorithm, key)).asResult)
.transform
+ .asEither
+ .leftMap(e => DeserializationError.DecryptionError(this.show, e.asErrorPathMessageStrings.toMap))
}
}
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Delete(
+ final case class Delete(
id: ID[User],
moderatorID: Option[ID[User]],
deletedAt: ModificationTime,
correlationID: CorrelationID
- ) extends UserCommandEvent
+ ) extends UserCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
}
diff --git a/modules/users-impl/src/main/scala/io/branchtalk/users/events/UsersCommandEvent.scala b/modules/users-impl/src/main/scala/io/branchtalk/users/events/UsersCommandEvent.scala
index 035d8095..3517a434 100644
--- a/modules/users-impl/src/main/scala/io/branchtalk/users/events/UsersCommandEvent.scala
+++ b/modules/users-impl/src/main/scala/io/branchtalk/users/events/UsersCommandEvent.scala
@@ -1,11 +1,9 @@
package io.branchtalk.users.events
-import com.sksamuel.avro4s._
-import io.branchtalk.ADT
+import com.sksamuel.avro4s.*
import io.branchtalk.shared.model.{ FastEq, ShowPretty }
-import io.scalaland.catnip.Semi
-@Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) sealed trait UsersCommandEvent extends ADT
+sealed trait UsersCommandEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
object UsersCommandEvent {
final case class ForUser(user: UserCommandEvent) extends UsersCommandEvent
final case class ForBan(ban: BanCommandEvent) extends UsersCommandEvent
diff --git a/modules/users-impl/src/main/scala/io/branchtalk/users/infrastructure/DoobieExtensions.scala b/modules/users-impl/src/main/scala/io/branchtalk/users/infrastructure/DoobieExtensions.scala
index 280814cf..cb93fb9b 100644
--- a/modules/users-impl/src/main/scala/io/branchtalk/users/infrastructure/DoobieExtensions.scala
+++ b/modules/users-impl/src/main/scala/io/branchtalk/users/infrastructure/DoobieExtensions.scala
@@ -1,12 +1,11 @@
package io.branchtalk.users.infrastructure
-import cats.Id
-import com.github.plokhotnyuk.jsoniter_scala.core._
-import com.github.plokhotnyuk.jsoniter_scala.macros._
-import io.branchtalk.shared.infrastructure.DoobieSupport._
-import io.branchtalk.shared.model.{ ID, SensitiveData, UUID, branchtalkLocale }
-import io.branchtalk.users.model.{ Ban, Password, Permission, Permissions, Session }
-import io.estatico.newtype.Coercible
+import cats.{ Id, Show }
+import com.github.plokhotnyuk.jsoniter_scala.core.*
+import com.github.plokhotnyuk.jsoniter_scala.macros.*
+import io.branchtalk.shared.infrastructure.DoobieSupport.{ *, given }
+import io.branchtalk.shared.model.*
+import io.branchtalk.users.model.*
import org.postgresql.util.PGobject
import scala.annotation.nowarn
@@ -14,49 +13,73 @@ import scala.collection.compat.immutable.ArraySeq
object DoobieExtensions {
- implicit val banScopeTypeMeta: Meta[Ban.Scope.Type] =
- pgEnumString("user_ban_type", Ban.Scope.Type.withNameInsensitive, _.entryName.toLowerCase(branchtalkLocale))
+ private given [A: Show]: Show[Array[A]] = _.map(_.show).mkString(", ")
- implicit val passwordAlgorithmMeta: Meta[Password.Algorithm] =
- pgEnumString("password_algorithm",
- Password.Algorithm.withNameInsensitive,
- _.entryName.toLowerCase(branchtalkLocale)
- )
+ @SuppressWarnings(Array("org.wartremover.warts.Throw"))
+ given banScopeTypeMeta: Meta[Ban.Scope.Type] = pgEnumString(
+ "user_ban_type",
+ name =>
+ Ban.Scope.Type.values
+ .find(_.entryName.equalsIgnoreCase(name))
+ .getOrElse(throw new NoSuchElementException(show"$name is not a member of Enum (${Ban.Scope.Type.values})")),
+ _.entryName.toLowerCase(branchtalkLocale)
+ )
- implicit val sessionUsageTypeMeta: Meta[Session.Usage.Type] =
- pgEnumString("session_usage_type",
- Session.Usage.Type.withNameInsensitive,
- _.entryName.toLowerCase(branchtalkLocale)
- )
+ given passwordAlgorithmMeta: Meta[Password.Algorithm] = pgEnumString(
+ "password_algorithm",
+ Password.Algorithm.withNameInsensitive,
+ _.entryName.toLowerCase(branchtalkLocale)
+ )
+
+ given passwordHashMeta: Meta[Password.Hash] = Password.Hash.unsafeMakeF(Meta.apply)
+
+ given passwordSaltMeta: Meta[Password.Salt] = Password.Salt.unsafeMakeF(Meta.apply)
+
+ @SuppressWarnings(Array("org.wartremover.warts.Throw"))
+ given sessionUsageTypeMeta: Meta[Session.Usage.Type] = pgEnumString(
+ "session_usage_type",
+ name =>
+ Session.Usage.Type.values
+ .find(_.entryName.equalsIgnoreCase(name))
+ .getOrElse(
+ throw new NoSuchElementException(show"$name is not a member of Enum (${Session.Usage.Type.values})")
+ ),
+ _.entryName.toLowerCase(branchtalkLocale)
+ )
+
+ given sessionExpirationTime: Meta[Session.ExpirationTime] = Session.ExpirationTime.unsafeMakeF(Meta.apply)
- implicit val sensitiveDataAlgorithmTypeMeta: Meta[SensitiveData.Algorithm] =
- pgEnumString("data_encryption_algorithm",
- SensitiveData.Algorithm.withNameInsensitive,
- _.entryName.toLowerCase(branchtalkLocale)
+ @SuppressWarnings(Array("org.wartremover.warts.Throw"))
+ given sensitiveDataAlgorithmTypeMeta: Meta[SensitiveData.Algorithm] =
+ pgEnumString(
+ "data_encryption_algorithm",
+ name =>
+ SensitiveData.Algorithm.values
+ .find(_.entryName.equalsIgnoreCase(name))
+ .getOrElse(
+ throw new NoSuchElementException(show"$name is not a member of Enum (${SensitiveData.Algorithm.values})")
+ ),
+ _.entryName.toLowerCase(branchtalkLocale)
)
- @SuppressWarnings(Array("org.wartremover.warts.All")) // coercible
- implicit val sensitiveDataKey: Meta[SensitiveData.Key] =
- Meta[Array[Byte]].timap[ArraySeq[Byte]](ArraySeq.from(_))(a => a.toArray).asInstanceOf[Meta[SensitiveData.Key]]
+ given sensitiveDataKey: Meta[SensitiveData.Key] =
+ SensitiveData.Key.unsafeMakeF[Meta](Meta[Array[Byte]].timap[ArraySeq[Byte]](ArraySeq.from(_))(a => a.toArray))
- @nowarn("cat=unused") // macros
- @SuppressWarnings(Array("org.wartremover.warts.All")) // macros
- implicit private def idCodec[A](implicit ev: Coercible[UUID, ID[A]]): JsonValueCodec[ID[A]] =
- Coercible.unsafeWrapMM[JsonValueCodec, Id, UUID, ID[A]].apply(JsonCodecMaker.make)
- @SuppressWarnings(Array("org.wartremover.warts.All")) // macros
- implicit private val permissionCodec: JsonValueCodec[Permission] = JsonCodecMaker.make[Permission]
- @SuppressWarnings(Array("org.wartremover.warts.All")) // macros
- implicit private val permissionsCodec: JsonValueCodec[Permissions] =
- Coercible[JsonValueCodec[Set[Permission]], JsonValueCodec[Permissions]].apply(JsonCodecMaker.make)
+ private given JsonValueCodec[Permission] = {
+ given [A]: JsonValueCodec[ID[A]] = ID.unsafeMakeF[JsonValueCodec, A](JsonCodecMaker.make[UUID])
+ JsonCodecMaker.make[Permission]
+ }
+ private given JsonValueCodec[Permissions] =
+ Permissions.unsafeMakeF[JsonValueCodec](JsonCodecMaker.make[Set[Permission]])
private val jsonType = "jsonb"
- implicit val permissionMeta: Meta[Permission] =
+ given permissionMeta: Meta[Permission] =
Meta.Advanced.other[PGobject](jsonType).timap[Permission](pgObj => readFromString[Permission](pgObj.getValue)) {
permission => new PGobject().tap(_.setType(jsonType)).tap(_.setValue(writeToString(permission)))
}
- implicit val permissionsMeta: Meta[Permissions] =
+ given permissionsMeta: Meta[Permissions] =
// imap instead of timap because a @newtype cannot have TypeTag
Meta.Advanced.other[PGobject](jsonType).imap[Permissions](pgObj => readFromString[Permissions](pgObj.getValue)) {
permissions => new PGobject().tap(_.setType(jsonType)).tap(_.setValue(writeToString(permissions)))
diff --git a/modules/users-impl/src/main/scala/io/branchtalk/users/model/BanDao.scala b/modules/users-impl/src/main/scala/io/branchtalk/users/model/BanDao.scala
index 6e9b3c26..010e4ab1 100644
--- a/modules/users-impl/src/main/scala/io/branchtalk/users/model/BanDao.scala
+++ b/modules/users-impl/src/main/scala/io/branchtalk/users/model/BanDao.scala
@@ -1,7 +1,7 @@
package io.branchtalk.users.model
import io.branchtalk.shared.model.{ ID, UUID }
-import io.scalaland.chimney.dsl._
+import io.scalaland.chimney.dsl.*
final case class BanDao(
bannedUserID: ID[User],
diff --git a/modules/users-impl/src/main/scala/io/branchtalk/users/model/SessionDao.scala b/modules/users-impl/src/main/scala/io/branchtalk/users/model/SessionDao.scala
index d9998aec..3abe6aeb 100644
--- a/modules/users-impl/src/main/scala/io/branchtalk/users/model/SessionDao.scala
+++ b/modules/users-impl/src/main/scala/io/branchtalk/users/model/SessionDao.scala
@@ -1,7 +1,7 @@
package io.branchtalk.users.model
import io.branchtalk.shared.model.ID
-import io.scalaland.chimney.dsl._
+import io.scalaland.chimney.dsl.*
final case class SessionDao(
id: ID[Session],
diff --git a/modules/users-impl/src/main/scala/io/branchtalk/users/model/UserDao.scala b/modules/users-impl/src/main/scala/io/branchtalk/users/model/UserDao.scala
index 3dd3bd90..9ae517af 100644
--- a/modules/users-impl/src/main/scala/io/branchtalk/users/model/UserDao.scala
+++ b/modules/users-impl/src/main/scala/io/branchtalk/users/model/UserDao.scala
@@ -1,7 +1,7 @@
package io.branchtalk.users.model
import io.branchtalk.shared.model.{ CreationTime, ID, ModificationTime }
-import io.scalaland.chimney.dsl._
+import io.scalaland.chimney.dsl.*
final case class UserDao(
id: ID[User],
diff --git a/modules/users-impl/src/main/scala/io/branchtalk/users/reads/BanReadsImpl.scala b/modules/users-impl/src/main/scala/io/branchtalk/users/reads/BanReadsImpl.scala
index f5f7d4a8..de034d86 100644
--- a/modules/users-impl/src/main/scala/io/branchtalk/users/reads/BanReadsImpl.scala
+++ b/modules/users-impl/src/main/scala/io/branchtalk/users/reads/BanReadsImpl.scala
@@ -1,16 +1,14 @@
package io.branchtalk.users.reads
import cats.effect.Sync
-import io.branchtalk.shared.infrastructure.DoobieSupport._
+import io.branchtalk.shared.infrastructure.DoobieSupport.{ *, given }
import io.branchtalk.shared.infrastructure.DoobieSupport.Fragments.whereAnd
import io.branchtalk.shared.model.ID
-import io.branchtalk.users.infrastructure.DoobieExtensions._
+import io.branchtalk.users.infrastructure.DoobieExtensions.{ *, given }
import io.branchtalk.users.model.{ Ban, BanDao, Channel, User }
final class BanReadsImpl[F[_]: Sync](transactor: Transactor[F]) extends BanReads[F] {
- implicit private val logHandler: LogHandler = doobieLogger(getClass)
-
private val channelBan: Ban.Scope.Type = Ban.Scope.Type.ForChannel
private val globalBan: Ban.Scope.Type = Ban.Scope.Type.Globally
@@ -22,15 +20,23 @@ final class BanReadsImpl[F[_]: Sync](transactor: Transactor[F]) extends BanReads
|FROM bans""".stripMargin
override def findForUser(userID: ID[User]): F[Set[Ban]] =
- (commonSelect ++ whereAnd(fr"user_id = $userID")).query[BanDao].map(_.toDomain).to[Set].transact(transactor)
+ (commonSelect ++ whereAnd(fr"user_id = $userID"))
+ .queryWithLabel[BanDao](show"Require Users' Ban for User=$userID")
+ .map(_.toDomain)
+ .to[Set]
+ .transact(transactor)
override def findForChannel(channelID: ID[Channel]): F[Set[Ban]] =
(commonSelect ++ whereAnd(fr"ban_id = $channelID", fr"ban_type = $channelBan"))
- .query[BanDao]
+ .queryWithLabel[BanDao](show"Require Users' Ban for Channel=$channelID")
.map(_.toDomain)
.to[Set]
.transact(transactor)
override def findGlobally: F[Set[Ban]] =
- (commonSelect ++ whereAnd(fr"ban_type = $globalBan")).query[BanDao].map(_.toDomain).to[Set].transact(transactor)
+ (commonSelect ++ whereAnd(fr"ban_type = $globalBan"))
+ .queryWithLabel[BanDao]("Require Users' Ban globally")
+ .map(_.toDomain)
+ .to[Set]
+ .transact(transactor)
}
diff --git a/modules/users-impl/src/main/scala/io/branchtalk/users/reads/SessionReadsImpl.scala b/modules/users-impl/src/main/scala/io/branchtalk/users/reads/SessionReadsImpl.scala
index 3c88d0bd..f294310a 100644
--- a/modules/users-impl/src/main/scala/io/branchtalk/users/reads/SessionReadsImpl.scala
+++ b/modules/users-impl/src/main/scala/io/branchtalk/users/reads/SessionReadsImpl.scala
@@ -1,18 +1,12 @@
package io.branchtalk.users.reads
import cats.effect.Sync
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.numeric.{ NonNegative, Positive }
-import io.branchtalk.shared.infrastructure.DoobieSupport._
+import io.branchtalk.shared.infrastructure.DoobieSupport.{ *, given }
import io.branchtalk.shared.model.{ ID, Paginated }
-import io.branchtalk.users.infrastructure.DoobieExtensions._
+import io.branchtalk.users.infrastructure.DoobieExtensions.{ *, given }
import io.branchtalk.users.model.{ Session, SessionDao, User }
-final class SessionReadsImpl[F[_]: Sync](
- transactor: Transactor[F]
-) extends SessionReads[F] {
-
- implicit private val logHandler: LogHandler = doobieLogger(getClass)
+final class SessionReadsImpl[F[_]: Sync](transactor: Transactor[F]) extends SessionReads[F] {
private val commonSelect: Fragment =
fr"""SELECT id,
@@ -29,17 +23,17 @@ final class SessionReadsImpl[F[_]: Sync](
override def paginate(
user: ID[User],
sortBy: Session.Sorting,
- offset: Long Refined NonNegative,
- limit: Int Refined Positive
+ offset: Paginated.Offset,
+ limit: Paginated.Limit
): F[Paginated[Session]] =
- (commonSelect ++ fr"WHERE user_id = ${user}" ++ orderBy(sortBy))
- .paginate[SessionDao](offset, limit)
+ (commonSelect ++ fr"WHERE user_id = $user" ++ orderBy(sortBy))
+ .paginate[SessionDao](offset, limit, show"Paginate Users' Session from $offset taking $limit sorted by $sortBy")
.map(_.map(_.toDomain))
.transact(transactor)
override def requireById(id: ID[Session]): F[Session] =
- (commonSelect ++ fr"WHERE id = ${id}")
- .query[SessionDao]
+ (commonSelect ++ fr"WHERE id = $id")
+ .queryWithLabel[SessionDao](show"Require Users' Session by ID=$id")
.map(_.toDomain)
.failNotFound("Session", id)
.transact(transactor)
diff --git a/modules/users-impl/src/main/scala/io/branchtalk/users/reads/UserReadsImpl.scala b/modules/users-impl/src/main/scala/io/branchtalk/users/reads/UserReadsImpl.scala
index 73c7cde5..729b7b6d 100644
--- a/modules/users-impl/src/main/scala/io/branchtalk/users/reads/UserReadsImpl.scala
+++ b/modules/users-impl/src/main/scala/io/branchtalk/users/reads/UserReadsImpl.scala
@@ -1,18 +1,13 @@
package io.branchtalk.users.reads
import cats.effect.Sync
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.numeric.{ NonNegative, Positive }
-import io.branchtalk.shared.infrastructure.DoobieSupport._
-import io.branchtalk.shared.model._
-import io.branchtalk.shared.model.Paginated
-import io.branchtalk.users.infrastructure.DoobieExtensions._
-import io.branchtalk.users.model.{ Password, User, UserDao }
+import io.branchtalk.shared.infrastructure.DoobieSupport.{ *, given }
+import io.branchtalk.shared.model.*
+import io.branchtalk.users.infrastructure.DoobieExtensions.{ *, given }
+import io.branchtalk.users.model.*
final class UserReadsImpl[F[_]: Sync](transactor: Transactor[F]) extends UserReads[F] {
- implicit private val logHandler: LogHandler = doobieLogger(getClass)
-
private val commonSelect: Fragment =
fr"""SELECT id,
| email,
@@ -37,11 +32,11 @@ final class UserReadsImpl[F[_]: Sync](transactor: Transactor[F]) extends UserRea
case User.Sorting.EmailAlphabetically => fr"ORDER BY email ASC"
}
- private def idExists(id: ID[User]): Fragment = fr"id = ${id}"
+ private def idExists(id: ID[User]): Fragment = fr"id = $id"
override def authenticate(username: User.Name, password: Password.Raw): F[User] =
(commonSelect ++ fr"WHERE username = ${username}")
- .query[UserDao]
+ .queryWithLabel[UserDao](show"Authenticate Users' User for Name=${username}")
.map(_.toDomain)
.option
.transact(transactor)
@@ -54,27 +49,33 @@ final class UserReadsImpl[F[_]: Sync](transactor: Transactor[F]) extends UserRea
override def paginate(
sortBy: User.Sorting,
- offset: Long Refined NonNegative,
- limit: Int Refined Positive,
+ offset: Paginated.Offset,
+ limit: Paginated.Limit,
filters: List[User.Filter] = List.empty
): F[Paginated[User]] =
- (commonSelect ++ Fragments.whereAnd(filters.map(filtered): _*) ++ orderBy(sortBy))
- .paginate[UserDao](offset, limit)
+ (commonSelect ++ Fragments.whereAndOpt(filters.map(filtered)) ++ orderBy(sortBy))
+ .paginate[UserDao](offset, limit, show"Paginate Users' Session from $offset taking $limit sorted by $sortBy")
.map(_.map(_.toDomain))
.transact(transactor)
override def exists(id: ID[User]): F[Boolean] =
- (fr"SELECT 1 FROM users WHERE" ++ idExists(id)).exists.transact(transactor)
+ (fr"SELECT 1 FROM users WHERE" ++ idExists(id)).exists(show"Users' User ID=$id exists").transact(transactor)
override def deleted(id: ID[User]): F[Boolean] =
- (fr"SELECT 1 FROM deleted_users WHERE" ++ idExists(id)).exists.transact(transactor)
+ (fr"SELECT 1 FROM deleted_users WHERE" ++ idExists(id))
+ .exists(show"Users' User ID=$id deleted")
+ .transact(transactor)
override def getById(id: ID[User]): F[Option[User]] =
- (commonSelect ++ fr"WHERE" ++ idExists(id)).query[UserDao].map(_.toDomain).option.transact(transactor)
+ (commonSelect ++ fr"WHERE" ++ idExists(id))
+ .queryWithLabel[UserDao](show"Get Users' User by ID=$id")
+ .map(_.toDomain)
+ .option
+ .transact(transactor)
override def requireById(id: ID[User]): F[User] =
(commonSelect ++ fr"WHERE" ++ idExists(id))
- .query[UserDao]
+ .queryWithLabel[UserDao](show"Require Users' User by ID=$id")
.map(_.toDomain)
.failNotFound("User", id)
.transact(transactor)
diff --git a/modules/users-impl/src/main/scala/io/branchtalk/users/writes/BanCommandHandler.scala b/modules/users-impl/src/main/scala/io/branchtalk/users/writes/BanCommandHandler.scala
index 30bd659f..ca2606be 100644
--- a/modules/users-impl/src/main/scala/io/branchtalk/users/writes/BanCommandHandler.scala
+++ b/modules/users-impl/src/main/scala/io/branchtalk/users/writes/BanCommandHandler.scala
@@ -6,7 +6,7 @@ import fs2.Stream
import io.branchtalk.shared.infrastructure.Projector
import io.branchtalk.shared.model.UUID
import io.branchtalk.users.events.{ BanCommandEvent, BanEvent, UsersCommandEvent, UsersEvent }
-import io.scalaland.chimney.dsl._
+import io.scalaland.chimney.dsl.*
final class BanCommandHandler[F[_]: Sync] extends Projector[F, UsersCommandEvent, (UUID, UsersEvent)] {
@@ -26,8 +26,8 @@ final class BanCommandHandler[F[_]: Sync] extends Projector[F, UsersCommandEvent
}
def toOrder(event: BanCommandEvent.OrderBan): F[(UUID, BanEvent.Banned)] =
- (event.bannedUserID.uuid -> event.transformInto[BanEvent.Banned]).pure[F]
+ (event.bannedUserID.unwrap -> event.transformInto[BanEvent.Banned]).pure[F]
def toLift(event: BanCommandEvent.LiftBan): F[(UUID, BanEvent.Unbanned)] =
- (event.bannedUserID.uuid -> event.transformInto[BanEvent.Unbanned]).pure[F]
+ (event.bannedUserID.unwrap -> event.transformInto[BanEvent.Unbanned]).pure[F]
}
diff --git a/modules/users-impl/src/main/scala/io/branchtalk/users/writes/BanPostgresProjector.scala b/modules/users-impl/src/main/scala/io/branchtalk/users/writes/BanPostgresProjector.scala
index 5cd12d82..de0af9b1 100644
--- a/modules/users-impl/src/main/scala/io/branchtalk/users/writes/BanPostgresProjector.scala
+++ b/modules/users-impl/src/main/scala/io/branchtalk/users/writes/BanPostgresProjector.scala
@@ -4,21 +4,18 @@ import cats.effect.Sync
import com.typesafe.scalalogging.Logger
import fs2.Stream
import io.branchtalk.logging.MDC
-import io.branchtalk.shared.infrastructure.DoobieSupport._
+import io.branchtalk.shared.infrastructure.DoobieSupport.{ *, given }
import io.branchtalk.shared.infrastructure.Projector
import io.branchtalk.shared.model.UUID
import io.branchtalk.users.events.{ BanEvent, UsersEvent }
-import io.branchtalk.users.infrastructure.DoobieExtensions._
+import io.branchtalk.users.infrastructure.DoobieExtensions.{ *, given }
import io.branchtalk.users.model.Ban
-import io.branchtalk.users.model.BanProperties.Scope
final class BanPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
extends Projector[F, UsersEvent, (UUID, UsersEvent)] {
private val logger = Logger(getClass)
- implicit private val logHandler: LogHandler = doobieLogger(getClass)
-
override def apply(in: Stream[F, UsersEvent]): Stream[F, (UUID, UsersEvent)] =
in.collect { case UsersEvent.ForBan(event) =>
event
@@ -46,7 +43,7 @@ final class BanPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
| ${banType},
| ${banID},
| ${event.reason}
- |)""".stripMargin.update.run.as(event.bannedUserID.uuid -> event).transact(transactor)
+ |)""".stripMargin.update.run.as(event.bannedUserID.unwrap -> event).transact(transactor)
}
@@ -54,15 +51,15 @@ final class BanPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
withCorrelationID(event.correlationID) {
val Ban.Scope.Tupled(banType, _) = event.scope
(event.scope match {
- case Scope.ForChannel(channelID) =>
+ case Ban.Scope.ForChannel(channelID) =>
sql"""DELETE FROM bans
|WHERE user_id = ${event.bannedUserID}
| AND ban_type = $banType
| AND ban_id = $channelID""".stripMargin
- case Scope.Globally =>
+ case Ban.Scope.Globally =>
sql"""DELETE FROM bans
|WHERE user_id = ${event.bannedUserID}
| AND ban_type = $banType""".stripMargin
- }).update.run.as(event.bannedUserID.uuid -> event).transact(transactor)
+ }).update.run.as(event.bannedUserID.unwrap -> event).transact(transactor)
}
}
diff --git a/modules/users-impl/src/main/scala/io/branchtalk/users/writes/BanWritesImpl.scala b/modules/users-impl/src/main/scala/io/branchtalk/users/writes/BanWritesImpl.scala
index 84d1e835..9c878b99 100644
--- a/modules/users-impl/src/main/scala/io/branchtalk/users/writes/BanWritesImpl.scala
+++ b/modules/users-impl/src/main/scala/io/branchtalk/users/writes/BanWritesImpl.scala
@@ -2,27 +2,26 @@ package io.branchtalk.users.writes
import cats.effect.Sync
import io.branchtalk.logging.{ CorrelationID, MDC }
-import io.branchtalk.shared.infrastructure.DoobieSupport._
-import io.branchtalk.shared.infrastructure.{ EventBusProducer, Writes }
-import io.branchtalk.shared.model.{ CreationTime, ModificationTime, UUIDGenerator }
+import io.branchtalk.shared.infrastructure.DoobieSupport.{ *, given }
+import io.branchtalk.shared.infrastructure.{ KafkaEventBus, Writes }
+import io.branchtalk.shared.model.{ CreationTime, ModificationTime, UUID }
import io.branchtalk.users.events.{ BanCommandEvent, UsersCommandEvent }
import io.branchtalk.users.model.{ Ban, User }
-import io.scalaland.chimney.dsl._
+import io.scalaland.chimney.dsl.*
final class BanWritesImpl[F[_]: Sync: MDC](
- producer: EventBusProducer[F, UsersCommandEvent],
+ producer: KafkaEventBus.Producer[F, UsersCommandEvent],
transactor: Transactor[F]
-)(implicit
- uuidGenerator: UUIDGenerator
-) extends Writes[F, User, UsersCommandEvent](producer)
- with BanWrites[F] {
+)(using UUID.Generator)
+ extends Writes[F, User, UsersCommandEvent](producer),
+ BanWrites[F] {
private val userCheck = new EntityCheck("User", transactor)
override def orderBan(order: Ban.Order): F[Unit] = for {
correlationID <- CorrelationID.getCurrentOrGenerate[F]
id = order.bannedUserID
- _ <- userCheck(id, sql"""SELECT 1 FROM users WHERE id = ${id}""")
+ _ <- userCheck(id, sql"""SELECT 1 FROM users WHERE id = $id""")
now <- CreationTime.now[F]
command = order
.into[BanCommandEvent.OrderBan]
@@ -35,7 +34,7 @@ final class BanWritesImpl[F[_]: Sync: MDC](
override def liftBan(lift: Ban.Lift): F[Unit] = for {
correlationID <- CorrelationID.getCurrentOrGenerate[F]
id = lift.bannedUserID
- _ <- userCheck(id, sql"""SELECT 1 FROM users WHERE id = ${id}""")
+ _ <- userCheck(id, sql"""SELECT 1 FROM users WHERE id = $id""")
now <- ModificationTime.now[F]
command = lift
.into[BanCommandEvent.LiftBan]
diff --git a/modules/users-impl/src/main/scala/io/branchtalk/users/writes/DiscussionsConsumer.scala b/modules/users-impl/src/main/scala/io/branchtalk/users/writes/DiscussionsConsumer.scala
index c86405b1..31530ffb 100644
--- a/modules/users-impl/src/main/scala/io/branchtalk/users/writes/DiscussionsConsumer.scala
+++ b/modules/users-impl/src/main/scala/io/branchtalk/users/writes/DiscussionsConsumer.scala
@@ -13,18 +13,18 @@ final class DiscussionsConsumer[F[_]: Sync] extends Projector[F, DiscussionEvent
override def apply(in: Stream[F, DiscussionEvent]): Stream[F, (UUID, UsersCommandEvent)] =
in.collect { case DiscussionEvent.ForChannel(created: ChannelEvent.Created) =>
val event = toGrantedChannelModerator(created)
- event.id.uuid -> UsersCommandEvent.ForUser(event)
+ event.id.unwrap -> UsersCommandEvent.ForUser(event)
}
def toGrantedChannelModerator(created: ChannelEvent.Created): UserCommandEvent.Update.Encrypted =
UserCommandEvent.Update.Encrypted(
- id = ID[User](created.authorID.uuid),
+ id = ID[User](created.authorID.unwrap),
moderatorID = None,
newUsername = Updatable.Keep,
newDescription = OptionUpdatable.Keep,
newPassword = Updatable.Keep,
- updatePermissions = List(Permission.Update.Add(Permission.ModerateChannel(ID[Channel](created.id.uuid)))),
- modifiedAt = ModificationTime(created.createdAt.offsetDateTime),
+ updatePermissions = List(Permission.Update.Add(Permission.ModerateChannel(ID[Channel](created.id.unwrap)))),
+ modifiedAt = ModificationTime(created.createdAt.unwrap),
correlationID = created.correlationID
)
}
diff --git a/modules/users-impl/src/main/scala/io/branchtalk/users/writes/SessionWritesImpl.scala b/modules/users-impl/src/main/scala/io/branchtalk/users/writes/SessionWritesImpl.scala
index 3d942e2f..484552c4 100644
--- a/modules/users-impl/src/main/scala/io/branchtalk/users/writes/SessionWritesImpl.scala
+++ b/modules/users-impl/src/main/scala/io/branchtalk/users/writes/SessionWritesImpl.scala
@@ -2,27 +2,24 @@ package io.branchtalk.users.writes
import cats.effect.Sync
import io.branchtalk.logging.{ CorrelationID, MDC }
-import io.branchtalk.shared.infrastructure.DoobieSupport._
-import io.branchtalk.shared.infrastructure.{ EventBusProducer, Writes }
-import io.branchtalk.shared.model.{ ID, UUIDGenerator }
+import io.branchtalk.shared.infrastructure.DoobieSupport.{ *, given }
+import io.branchtalk.shared.infrastructure.{ KafkaEventBus, Writes }
+import io.branchtalk.shared.model.{ ID, UUID }
import io.branchtalk.users.events.{ SessionEvent, UsersEvent }
-import io.branchtalk.users.infrastructure.DoobieExtensions._
+import io.branchtalk.users.infrastructure.DoobieExtensions.{ *, given }
import io.branchtalk.users.model.{ Session, SessionDao }
import io.branchtalk.users.reads.SessionReadsImpl
-import io.scalaland.chimney.dsl._
+import io.scalaland.chimney.dsl.*
final class SessionWritesImpl[F[_]: Sync: MDC](
- producer: EventBusProducer[F, UsersEvent],
+ producer: KafkaEventBus.Producer[F, UsersEvent],
transactor: Transactor[F]
-)(implicit
- uuidGenerator: UUIDGenerator
-) extends Writes[F, Session, UsersEvent](producer)
- with SessionWrites[F] {
+)(using UUID.Generator)
+ extends Writes[F, Session, UsersEvent](producer),
+ SessionWrites[F] {
private val reads = new SessionReadsImpl[F](transactor)
- implicit private val logHandler: LogHandler = doobieLogger(getClass)
-
override def createSession(newSession: Session.Create): F[Session] =
for {
id <- ID.create[F, Session]
diff --git a/modules/users-impl/src/main/scala/io/branchtalk/users/writes/UserCommandHandler.scala b/modules/users-impl/src/main/scala/io/branchtalk/users/writes/UserCommandHandler.scala
index 98540f6a..140b743e 100644
--- a/modules/users-impl/src/main/scala/io/branchtalk/users/writes/UserCommandHandler.scala
+++ b/modules/users-impl/src/main/scala/io/branchtalk/users/writes/UserCommandHandler.scala
@@ -6,7 +6,7 @@ import fs2.Stream
import io.branchtalk.shared.infrastructure.Projector
import io.branchtalk.shared.model.UUID
import io.branchtalk.users.events.{ UserCommandEvent, UserEvent, UsersCommandEvent, UsersEvent }
-import io.scalaland.chimney.dsl._
+import io.scalaland.chimney.dsl.*
final class UserCommandHandler[F[_]: Sync] extends Projector[F, UsersCommandEvent, (UUID, UsersEvent)] {
@@ -27,11 +27,11 @@ final class UserCommandHandler[F[_]: Sync] extends Projector[F, UsersCommandEven
}
def toCreate(command: UserCommandEvent.Create.Encrypted): F[(UUID, UserEvent.Created.Encrypted)] =
- (command.id.uuid -> command.transformInto[UserEvent.Created.Encrypted]).pure[F]
+ (command.id.unwrap -> command.transformInto[UserEvent.Created.Encrypted]).pure[F]
def toUpdate(command: UserCommandEvent.Update.Encrypted): F[(UUID, UserEvent.Updated.Encrypted)] =
- (command.id.uuid -> command.transformInto[UserEvent.Updated.Encrypted]).pure[F]
+ (command.id.unwrap -> command.transformInto[UserEvent.Updated.Encrypted]).pure[F]
def toDelete(command: UserCommandEvent.Delete): F[(UUID, UserEvent.Deleted)] =
- (command.id.uuid -> command.transformInto[UserEvent.Deleted]).pure[F]
+ (command.id.unwrap -> command.transformInto[UserEvent.Deleted]).pure[F]
}
diff --git a/modules/users-impl/src/main/scala/io/branchtalk/users/writes/UserPostgresProjector.scala b/modules/users-impl/src/main/scala/io/branchtalk/users/writes/UserPostgresProjector.scala
index d46ad640..cbbf31f8 100644
--- a/modules/users-impl/src/main/scala/io/branchtalk/users/writes/UserPostgresProjector.scala
+++ b/modules/users-impl/src/main/scala/io/branchtalk/users/writes/UserPostgresProjector.scala
@@ -5,11 +5,11 @@ import cats.effect.Sync
import com.typesafe.scalalogging.Logger
import fs2.Stream
import io.branchtalk.logging.MDC
-import io.branchtalk.shared.infrastructure.DoobieSupport._
+import io.branchtalk.shared.infrastructure.DoobieSupport.{ *, given }
import io.branchtalk.shared.infrastructure.Projector
import io.branchtalk.shared.model.{ ID, SensitiveData, UUID }
import io.branchtalk.users.events.{ UserEvent, UsersEvent }
-import io.branchtalk.users.infrastructure.DoobieExtensions._
+import io.branchtalk.users.infrastructure.DoobieExtensions.{ *, given }
import io.branchtalk.users.model.{ Permission, Permissions, Session, User }
final class UserPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
@@ -17,8 +17,6 @@ final class UserPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
private val logger = Logger(getClass)
- implicit private val logHandler: LogHandler = doobieLogger(getClass)
-
override def apply(in: Stream[F, UsersEvent]): Stream[F, (UUID, UsersEvent)] =
in.collect { case UsersEvent.ForUser(event) =>
event
@@ -34,7 +32,6 @@ final class UserPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
Stream.empty
}
- // scalastyle:off method.length
def toCreate(encrypted: UserEvent.Created.Encrypted): F[Option[(UUID, UserEvent.Created.Encrypted)]] =
withCorrelationID(encrypted.correlationID) {
findKeys(encrypted.id)
@@ -45,8 +42,12 @@ final class UserPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
val Session.Usage.Tupled(sessionType, sessionPermissions) = Session.Usage.UserSession
- sql"DELETE FROM reserved_emails WHERE email = ${event.email.value}".update.run >>
- sql"DELETE FROM reserved_usernames WHERE username = ${event.username.value}".update.run >>
+ sql"DELETE FROM reserved_emails WHERE email = ${event.email.value}"
+ .updateWithLabel(show"Delete Users' Email reservation")
+ .run >>
+ sql"DELETE FROM reserved_usernames WHERE username = ${event.username.value}"
+ .updateWithLabel(show"Delete Users' Name reservation")
+ .run >>
sql"""INSERT INTO users (
| id,
| email,
@@ -69,7 +70,9 @@ final class UserPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
| ${Permissions(Set.empty)},
| ${event.createdAt}
|)
- |ON CONFLICT (id) DO NOTHING""".stripMargin.update.run >>
+ |ON CONFLICT (id) DO NOTHING""".stripMargin
+ .updateWithLabel(show"Create Users' User ID=${event.id}")
+ .run >>
sql"""INSERT INTO sessions (
| id,
| user_id,
@@ -86,12 +89,10 @@ final class UserPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
|)""".stripMargin.update.run
}
)
- .as((encrypted.id.uuid -> encrypted).some)
+ .as((encrypted.id.unwrap -> encrypted).some)
.transact(transactor)
}
- // scalastyle:on method.length
- // scalastyle:off method.length
def toUpdate(encrypted: UserEvent.Updated.Encrypted): F[Option[(UUID, UserEvent.Updated.Encrypted)]] =
withCorrelationID(encrypted.correlationID) {
findKeys(encrypted.id)
@@ -99,17 +100,20 @@ final class UserPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
_.traverse { case (algorithm, key) =>
@SuppressWarnings(Array("org.wartremover.warts.Throw"))
val event = encrypted.decrypt(algorithm, key).fold(e => throw new Exception(e.show), identity)
- import event._
+ import event.*
val defaultPermissions = Permissions.empty
val permissionsUpdateNel = NonEmptyList.fromList(updatePermissions)
- val cleanReservedIfNecessary = event.newUsername.toOption
- .traverse(username => sql"DELETE FROM reserved_usernames WHERE username = ${username.value}".update.run)
+ val cleanReservedIfNecessary = event.newUsername.toOption.traverse(username =>
+ sql"DELETE FROM reserved_usernames WHERE username = ${username.value}"
+ .updateWithLabel(show"Delete Users' Name=${username} reservation")
+ .run
+ )
val fetchPermissionsIfNecessary = permissionsUpdateNel.fold(defaultPermissions.pure[ConnectionIO]) { _ =>
sql"""SELECT permissions FROM users WHERE id = $id"""
- .query[Permissions]
+ .queryWithLabel[Permissions](show"Get Users' Permissions for ID=$id")
.option
.map(_.getOrElse(defaultPermissions))
}
@@ -128,9 +132,9 @@ final class UserPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
),
permissionsUpdateNel.map { nel =>
fr"""permissions = ${nel.foldLeft(existingPermissions) {
- case (permissions, Permission.Update.Add(permission)) => permissions.append(permission)
- case (permissions, Permission.Update.Remove(permission)) => permissions.remove(permission)
- }}"""
+ case (permissions, Permission.Update.Add(permission)) => permissions.append(permission)
+ case (permissions, Permission.Update.Remove(permission)) => permissions.remove(permission)
+ }}"""
}
).flatten.pipe(NonEmptyList.fromList) match {
case Some(updates) =>
@@ -143,12 +147,11 @@ final class UserPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
)
}
- (cleanReservedIfNecessary >> fetchPermissionsIfNecessary.flatMap(updateUser)).as(id.uuid -> encrypted)
+ (cleanReservedIfNecessary >> fetchPermissionsIfNecessary.flatMap(updateUser)).as(id.unwrap -> encrypted)
}
)
.transact(transactor)
}
- // scalastyle:on method.length
def toDelete(event: UserEvent.Deleted): F[Option[(UUID, UserEvent.Deleted)]] =
withCorrelationID(event.correlationID) {
@@ -157,11 +160,13 @@ final class UserPostgresProjector[F[_]: Sync: MDC](transactor: Transactor[F])
sql"""INSERT INTO deleted_users (id, deleted_at)
|VALUES (${event.id}, ${event.deletedAt})
ON CONFLICT (id) DO NOTHING""".stripMargin.update.run
- }.as((event.id.uuid -> event).some).transact(transactor)
+ }.as((event.id.unwrap -> event).some).transact(transactor)
}
private def findKeys(userID: ID[User]): ConnectionIO[Option[(SensitiveData.Algorithm, SensitiveData.Key)]] =
sql"""SELECT enc_algorithm, key_value FROM sensitive_data_keys WHERE user_id = $userID"""
- .query[(SensitiveData.Algorithm, SensitiveData.Key)]
+ .queryWithLabel[(SensitiveData.Algorithm, SensitiveData.Key)](
+ show"Get encryption keys for Users' User ID=$userID"
+ )
.option
}
diff --git a/modules/users-impl/src/main/scala/io/branchtalk/users/writes/UserWritesImpl.scala b/modules/users-impl/src/main/scala/io/branchtalk/users/writes/UserWritesImpl.scala
index bc2cd400..d2b54e78 100644
--- a/modules/users-impl/src/main/scala/io/branchtalk/users/writes/UserWritesImpl.scala
+++ b/modules/users-impl/src/main/scala/io/branchtalk/users/writes/UserWritesImpl.scala
@@ -3,55 +3,56 @@ package io.branchtalk.users.writes
import cats.data.NonEmptyList
import cats.effect.Sync
import io.branchtalk.logging.{ CorrelationID, MDC }
-import io.branchtalk.shared.infrastructure.DoobieSupport._
-import io.branchtalk.shared.infrastructure.{ EventBusProducer, Writes }
-import io.branchtalk.shared.model._
+import io.branchtalk.shared.infrastructure.*
+import io.branchtalk.shared.infrastructure.DoobieSupport.{ *, given }
+import io.branchtalk.shared.model.*
import io.branchtalk.users.events.{ UserCommandEvent, UsersCommandEvent }
-import io.branchtalk.users.infrastructure.DoobieExtensions._
+import io.branchtalk.users.infrastructure.DoobieExtensions.{ *, given }
import io.branchtalk.users.model.{ Session, User }
-import io.scalaland.chimney.dsl._
+import io.scalaland.chimney.dsl.*
final class UserWritesImpl[F[_]: Sync: MDC](
- producer: EventBusProducer[F, UsersCommandEvent],
+ producer: KafkaEventBus.Producer[F, UsersCommandEvent],
transactor: Transactor[F]
-)(implicit
- uuidGenerator: UUIDGenerator
-) extends Writes[F, User, UsersCommandEvent](producer)
- with UserWrites[F] {
-
- implicit private val logHandler: LogHandler = doobieLogger(getClass)
+)(using UUID.Generator)
+ extends Writes[F, User, UsersCommandEvent](producer),
+ UserWrites[F] {
private val sessionExpiresInDays = 7L // TODO: make it configurable
private val userCheck = new EntityCheck("User", transactor)
- private def reserveEmail(email: User.Email, id: Option[ID[User]] = None)(implicit pos: CodePosition): F[Unit] = {
+ private def reserveEmail(email: User.Email, id: Option[ID[User]] = None)(using CodePosition): F[Unit] = {
for {
isReserved <- sql"""SELECT 1 FROM users WHERE email = $email AND id <> $id
|UNION
|SELECT 1 FROM reserved_emails WHERE email = $email
- |""".stripMargin.exists
+ |""".stripMargin.exists(show"Check if Users' Email=$email is reserved for ID=$id")
_ <-
if (isReserved) {
- CommonError
- .ValidationFailed(NonEmptyList.one(show"Email $email already exists"), pos)
- .raiseError[ConnectionIO, Unit]
- } else sql"""INSERT INTO reserved_emails (email) VALUES ($email)""".update.run.void
+ CommonError.validationFailed(show"Email $email already exists").raiseError[ConnectionIO, Unit]
+ } else
+ sql"""INSERT INTO reserved_emails (email) VALUES ($email)"""
+ .updateWithLabel(show"Reserve Users' Email=$email for ID=$id")
+ .run
+ .void
} yield ()
}.transact(transactor)
- private def reserveUsername(name: User.Name, id: Option[ID[User]] = None)(implicit pos: CodePosition): F[Unit] = {
+ private def reserveUsername(name: User.Name, id: Option[ID[User]] = None)(using CodePosition): F[Unit] = {
for {
isReserved <- sql"""SELECT 1 FROM users WHERE username = $name AND id <> $id
|UNION
|SELECT 1 FROM reserved_usernames WHERE username = $name
- |""".stripMargin.exists
+ |""".stripMargin.exists(show"Check if Users' Name=$name is reserved for ID=$id")
_ <-
if (isReserved) {
- CommonError
- .ValidationFailed(NonEmptyList.one(show"Username $name already exists"), pos)
- .raiseError[ConnectionIO, Unit]
- } else sql"""INSERT INTO reserved_usernames (username) VALUES ($name)""".update.run.void
+ CommonError.validationFailed(show"Username $name already exists").raiseError[ConnectionIO, Unit]
+ } else
+ sql"""INSERT INTO reserved_usernames (username) VALUES ($name)"""
+ .updateWithLabel(show"Reserve Users' Name=$name for ID=$id")
+ .run
+ .void
} yield ()
}.transact(transactor)
@@ -72,7 +73,11 @@ final class UserWritesImpl[F[_]: Sync: MDC](
| $id,
| $key,
| $algorithm
- |)""".stripMargin.update.run.as(algorithm -> key).transact(transactor)
+ |)""".stripMargin
+ .updateWithLabel(show"Create Users' User ID=$id")
+ .run
+ .as(algorithm -> key)
+ .transact(transactor)
}
sessionID <- ID.create[F, Session]
now <- CreationTime.now[F]
@@ -84,7 +89,7 @@ final class UserWritesImpl[F[_]: Sync: MDC](
.withFieldComputed(_.password, _.password.pipe(SensitiveData(_)))
.withFieldConst(_.createdAt, now)
.withFieldConst(_.sessionID, sessionID)
- .withFieldConst(_.sessionExpiresAt, Session.ExpirationTime(now.offsetDateTime.plusDays(sessionExpiresInDays)))
+ .withFieldConst(_.sessionExpiresAt, Session.ExpirationTime(now.unwrap.plusDays(sessionExpiresInDays)))
.withFieldConst(_.correlationID, correlationID)
.transform
.encrypt(algorithm, key)
@@ -99,7 +104,9 @@ final class UserWritesImpl[F[_]: Sync: MDC](
_ <- userCheck(id, sql"""SELECT 1 FROM users WHERE id = $id""")
(algorithm, key) <-
sql"""SELECT enc_algorithm, key_value FROM sensitive_data_keys WHERE user_id = $id"""
- .query[(SensitiveData.Algorithm, SensitiveData.Key)]
+ .queryWithLabel[(SensitiveData.Algorithm, SensitiveData.Key)](
+ show"Get encryption keys for Users' User ID=$id"
+ )
.unique
.transact(transactor)
now <- ModificationTime.now[F]
@@ -118,7 +125,7 @@ final class UserWritesImpl[F[_]: Sync: MDC](
for {
correlationID <- CorrelationID.getCurrentOrGenerate[F]
id = deletedUser.id
- _ <- userCheck(id, sql"""SELECT 1 FROM users WHERE id = ${id}""")
+ _ <- userCheck(id, sql"""SELECT 1 FROM users WHERE id = $id""")
now <- ModificationTime.now[F]
command = deletedUser
.into[UserCommandEvent.Delete]
diff --git a/modules/users-impl/src/it/resources/users-test.conf b/modules/users-impl/src/test/resources/users-test.conf
similarity index 100%
rename from modules/users-impl/src/it/resources/users-test.conf
rename to modules/users-impl/src/test/resources/users-test.conf
diff --git a/modules/users-impl/src/it/scala/io/branchtalk/users/BanReadsWritesSpec.scala b/modules/users-impl/src/test/scala/io/branchtalk/users/BanReadsWritesSpec.scala
similarity index 71%
rename from modules/users-impl/src/it/scala/io/branchtalk/users/BanReadsWritesSpec.scala
rename to modules/users-impl/src/test/scala/io/branchtalk/users/BanReadsWritesSpec.scala
index 59d85747..57eefb2e 100644
--- a/modules/users-impl/src/it/scala/io/branchtalk/users/BanReadsWritesSpec.scala
+++ b/modules/users-impl/src/test/scala/io/branchtalk/users/BanReadsWritesSpec.scala
@@ -1,22 +1,20 @@
package io.branchtalk.users
-import cats.syntax.eq._
import io.branchtalk.shared.model.TestUUIDGenerator
import io.branchtalk.users.model.Ban
-import io.branchtalk.users.model.BanProperties.Scope
-import io.scalaland.chimney.dsl._
+import io.scalaland.chimney.dsl.*
import org.specs2.mutable.Specification
-final class BanReadsWritesSpec extends Specification with UsersIOTest with UsersFixtures {
+final class BanReadsWritesSpec extends Specification, UsersIOTest, UsersFixtures {
- implicit protected val uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
+ protected given uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
"Ban Reads & Writes" should {
"order a User's Ban and lift User's ban and eventually execute command" in {
for {
// given
- userID <- userCreate.flatMap(usersWrites.userWrites.createUser).map(_._1.id)
+ userID <- userCreate.flatMap(usersWrites.userWrites.createUser).map(_._1.unwrap)
_ <- usersReads.userReads.requireById(userID).eventually()
channelID <- channelIDCreate
expectedBans <- banCreate(userID, channelID).map(ban => List(ban, ban.copy(scope = Ban.Scope.Globally)))
@@ -37,20 +35,20 @@ final class BanReadsWritesSpec extends Specification with UsersIOTest with Users
.eventually()
} yield {
// then
- bansExecuted must_=== expectedBans.toSet
- channelBansExecuted must_=== expectedBans
+ bansExecuted === expectedBans.toSet
+ channelBansExecuted === expectedBans
.filter(_.scope match {
- case Scope.ForChannel(_) => true
- case Scope.Globally => false
+ case Ban.Scope.ForChannel(_) => true
+ case Ban.Scope.Globally => false
})
.toSet
- globalBansExecuted must_=== expectedBans
+ globalBansExecuted === expectedBans
.filter(_.scope match {
- case Scope.ForChannel(_) => false
- case Scope.Globally => true
+ case Ban.Scope.ForChannel(_) => false
+ case Ban.Scope.Globally => true
})
.toSet
- bansLifted must beEmpty
+ bansLifted.toSeq must beEmpty
}
}
}
diff --git a/modules/users-impl/src/it/scala/io/branchtalk/users/SessionReadsWritesSpec.scala b/modules/users-impl/src/test/scala/io/branchtalk/users/SessionReadsWritesSpec.scala
similarity index 71%
rename from modules/users-impl/src/it/scala/io/branchtalk/users/SessionReadsWritesSpec.scala
rename to modules/users-impl/src/test/scala/io/branchtalk/users/SessionReadsWritesSpec.scala
index 965fa7f7..79748167 100644
--- a/modules/users-impl/src/it/scala/io/branchtalk/users/SessionReadsWritesSpec.scala
+++ b/modules/users-impl/src/test/scala/io/branchtalk/users/SessionReadsWritesSpec.scala
@@ -1,19 +1,20 @@
package io.branchtalk.users
-import io.branchtalk.shared.model.{ CreationScheduled, TestUUIDGenerator }
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.infrastructure.*
import io.branchtalk.users.model.Session
import org.specs2.mutable.Specification
-final class SessionReadsWritesSpec extends Specification with UsersIOTest with UsersFixtures {
+final class SessionReadsWritesSpec extends Specification, UsersIOTest, UsersFixtures {
- implicit protected val uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
+ protected given uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
"Session Reads & Writes" should {
"create a Session and immediately read it" in {
for {
// given
- userID <- userCreate.flatMap(usersWrites.userWrites.createUser).map(_._1.id)
+ userID <- userCreate.flatMap(usersWrites.userWrites.createUser).map(_._1.unwrap)
_ <- usersReads.userReads.requireById(userID).eventually()
creationData <- (0 until 3).toList.traverse(_ => sessionCreate(userID))
// when
@@ -28,7 +29,7 @@ final class SessionReadsWritesSpec extends Specification with UsersIOTest with U
"allow immediate delete of a created Session" in {
for {
// given
- userID <- userCreate.flatMap(usersWrites.userWrites.createUser).map(_._1.id)
+ userID <- userCreate.flatMap(usersWrites.userWrites.createUser).map(_._1.unwrap)
_ <- usersReads.userReads.requireById(userID).eventually()
creationData <- (0 until 3).toList.traverse(_ => sessionCreate(userID))
toCreate <- creationData.traverse(usersWrites.sessionWrites.createSession)
@@ -69,14 +70,22 @@ final class SessionReadsWritesSpec extends Specification with UsersIOTest with U
paginatedData <- (0 until 19).toList.traverse(_ => sessionCreate(userID))
_ <- paginatedData.traverse(usersWrites.sessionWrites.createSession).map(_.map(_.id))
// when
- pagination <- usersReads.sessionReads.paginate(userID, Session.Sorting.ClosestToExpiry, 0L, 10)
- pagination2 <- usersReads.sessionReads.paginate(userID, Session.Sorting.ClosestToExpiry, 10L, 10)
+ pagination <- usersReads.sessionReads.paginate(userID,
+ Session.Sorting.ClosestToExpiry,
+ Paginated.Offset(0L),
+ Paginated.Limit(10)
+ )
+ pagination2 <- usersReads.sessionReads.paginate(userID,
+ Session.Sorting.ClosestToExpiry,
+ Paginated.Offset(10L),
+ Paginated.Limit(10)
+ )
} yield {
// then
pagination.entities must haveSize(10)
- pagination.nextOffset.map(_.value) must beSome(10L)
+ pagination.nextOffset.map(_.unwrap) must beSome(10L)
pagination2.entities must haveSize(10)
- pagination2.nextOffset.map(_.value) must beNone
+ pagination2.nextOffset.map(_.unwrap) must beNone
}
}
}
diff --git a/modules/users-impl/src/it/scala/io/branchtalk/users/TestUsersConfig.scala b/modules/users-impl/src/test/scala/io/branchtalk/users/TestUsersConfig.scala
similarity index 59%
rename from modules/users-impl/src/it/scala/io/branchtalk/users/TestUsersConfig.scala
rename to modules/users-impl/src/test/scala/io/branchtalk/users/TestUsersConfig.scala
index 6998d93c..3fffd1bf 100644
--- a/modules/users-impl/src/it/scala/io/branchtalk/users/TestUsersConfig.scala
+++ b/modules/users-impl/src/test/scala/io/branchtalk/users/TestUsersConfig.scala
@@ -1,23 +1,15 @@
package io.branchtalk.users
import cats.effect.{ Async, Resource, Sync }
-import io.branchtalk.shared.infrastructure.{
- DomainConfig,
- DomainName,
- KafkaEventConsumerConfig,
- TestKafkaEventBusConfig,
- TestPostgresConfig,
- TestResources
-}
-import io.scalaland.catnip.Semi
-import pureconfig.{ ConfigReader, ConfigSource }
+import io.branchtalk.shared.infrastructure.*
+import io.branchtalk.shared.infrastructure.PureconfigSupport.{ *, given }
-@Semi(ConfigReader) final case class TestUsersConfig(
+final case class TestUsersConfig(
database: TestPostgresConfig,
publishedEventBus: TestKafkaEventBusConfig,
internalEventBus: TestKafkaEventBusConfig,
- consumers: Map[String, KafkaEventConsumerConfig]
-)
+ consumers: Map[String, KafkaEventBus.ConsumerConfig]
+) derives ConfigReader
object TestUsersConfig {
def load[F[_]: Sync]: Resource[F, TestUsersConfig] =
@@ -27,11 +19,11 @@ object TestUsersConfig {
)
)
- def loadDomainConfig[F[_]: Async]: Resource[F, DomainConfig] =
+ def loadDomainConfig[F[_]: Async]: Resource[F, DomainModule.Config] =
for {
TestUsersConfig(dbTest, publishedESTest, internalESTest, consumers) <- TestUsersConfig.load[F]
db <- TestResources.postgresConfigResource[F](dbTest)
publishedES <- TestResources.kafkaEventBusConfigResource[F](publishedESTest)
internalES <- TestResources.kafkaEventBusConfigResource[F](internalESTest)
- } yield DomainConfig(DomainName("discussions-test"), db, db, publishedES, internalES, consumers)
+ } yield DomainModule.Config(DomainModule.Name("discussions-test"), db, db, publishedES, internalES, consumers)
}
diff --git a/modules/users-impl/src/it/scala/io/branchtalk/users/UserPaginationSpec.scala b/modules/users-impl/src/test/scala/io/branchtalk/users/UserPaginationSpec.scala
similarity index 67%
rename from modules/users-impl/src/it/scala/io/branchtalk/users/UserPaginationSpec.scala
rename to modules/users-impl/src/test/scala/io/branchtalk/users/UserPaginationSpec.scala
index f792f0d1..4eadf396 100644
--- a/modules/users-impl/src/it/scala/io/branchtalk/users/UserPaginationSpec.scala
+++ b/modules/users-impl/src/test/scala/io/branchtalk/users/UserPaginationSpec.scala
@@ -1,18 +1,18 @@
package io.branchtalk.users
-import io.branchtalk.shared.model.{ OptionUpdatable, TestUUIDGenerator, Updatable }
+import io.branchtalk.shared.model.*
import io.branchtalk.users.model.Permission.ModerateChannel
import io.branchtalk.users.model.{ Permission, Permissions, User }
import org.specs2.mutable.Specification
import scala.concurrent.duration.DurationInt
-final class UserPaginationSpec extends Specification with UsersIOTest with UsersFixtures {
+final class UserPaginationSpec extends Specification, UsersIOTest, UsersFixtures {
// User pagination tests cannot be run in parallel to other User tests (no parent to filter other tests)
sequential
- implicit protected val uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
+ protected given uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
"User pagination" should {
@@ -20,17 +20,17 @@ final class UserPaginationSpec extends Specification with UsersIOTest with Users
for {
// given
paginatedData <- (0 until 10).toList.traverse(_ => userCreate)
- paginatedIDs <- paginatedData.traverse(usersWrites.userWrites.createUser).map(_.map(_._1.id))
+ paginatedIDs <- paginatedData.traverse(usersWrites.userWrites.createUser).map(_.map(_._1.unwrap))
_ <- paginatedIDs
.traverse(usersReads.userReads.requireById(_))
.eventually(delay = 1.second, timeout = 30.seconds)
// when
- pagination <- usersReads.userReads.paginate(User.Sorting.Newest, 0L, 5)
- pagination2 <- usersReads.userReads.paginate(User.Sorting.Newest, 5L, 5)
+ pagination <- usersReads.userReads.paginate(User.Sorting.Newest, Paginated.Offset(0L), Paginated.Limit(5))
+ pagination2 <- usersReads.userReads.paginate(User.Sorting.Newest, Paginated.Offset(5L), Paginated.Limit(5))
} yield {
// then
pagination.entities must haveSize(5)
- pagination.nextOffset.map(_.value) must beSome(5L)
+ pagination.nextOffset.map(_.unwrap) must beSome(5L)
pagination2.entities must haveSize(5)
}
}
@@ -39,17 +39,23 @@ final class UserPaginationSpec extends Specification with UsersIOTest with Users
for {
// given
paginatedData <- (0 until 10).toList.traverse(_ => userCreate)
- paginatedIDs <- paginatedData.traverse(usersWrites.userWrites.createUser).map(_.map(_._1.id))
+ paginatedIDs <- paginatedData.traverse(usersWrites.userWrites.createUser).map(_.map(_._1.unwrap))
_ <- paginatedIDs
.traverse(usersReads.userReads.requireById(_))
.eventually(delay = 1.second, timeout = 30.seconds)
// when
- pagination <- usersReads.userReads.paginate(User.Sorting.NameAlphabetically, 0L, 5)
- pagination2 <- usersReads.userReads.paginate(User.Sorting.NameAlphabetically, 5L, 5)
+ pagination <- usersReads.userReads.paginate(User.Sorting.NameAlphabetically,
+ Paginated.Offset(0L),
+ Paginated.Limit(5)
+ )
+ pagination2 <- usersReads.userReads.paginate(User.Sorting.NameAlphabetically,
+ Paginated.Offset(5L),
+ Paginated.Limit(5)
+ )
} yield {
// then
pagination.entities must haveSize(5)
- pagination.nextOffset.map(_.value) must beSome(5L)
+ pagination.nextOffset.map(_.unwrap) must beSome(5L)
pagination2.entities must haveSize(5)
}
}
@@ -58,17 +64,23 @@ final class UserPaginationSpec extends Specification with UsersIOTest with Users
for {
// given
paginatedData <- (0 until 10).toList.traverse(_ => userCreate)
- paginatedIDs <- paginatedData.traverse(usersWrites.userWrites.createUser).map(_.map(_._1.id))
+ paginatedIDs <- paginatedData.traverse(usersWrites.userWrites.createUser).map(_.map(_._1.unwrap))
_ <- paginatedIDs
.traverse(usersReads.userReads.requireById(_))
.eventually(delay = 1.second, timeout = 30.seconds)
// when
- pagination <- usersReads.userReads.paginate(User.Sorting.EmailAlphabetically, 0L, 5)
- pagination2 <- usersReads.userReads.paginate(User.Sorting.EmailAlphabetically, 5L, 5)
+ pagination <- usersReads.userReads.paginate(User.Sorting.EmailAlphabetically,
+ Paginated.Offset(0L),
+ Paginated.Limit(5)
+ )
+ pagination2 <- usersReads.userReads.paginate(User.Sorting.EmailAlphabetically,
+ Paginated.Offset(5L),
+ Paginated.Limit(5)
+ )
} yield {
// then
pagination.entities must haveSize(5)
- pagination.nextOffset.map(_.value) must beSome(5L)
+ pagination.nextOffset.map(_.unwrap) must beSome(5L)
pagination2.entities must haveSize(5)
}
}
@@ -83,7 +95,7 @@ final class UserPaginationSpec extends Specification with UsersIOTest with Users
ModerateChannel(channelID)
)
creationdData <- permissions.traverse(_ => userCreate)
- paginatedIDs <- creationdData.traverse(usersWrites.userWrites.createUser).map(_.map(_._1.id))
+ paginatedIDs <- creationdData.traverse(usersWrites.userWrites.createUser).map(_.map(_._1.unwrap))
_ <- paginatedIDs
.traverse(usersReads.userReads.requireById(_))
.eventually(delay = 1.second, timeout = 30.seconds)
@@ -104,19 +116,24 @@ final class UserPaginationSpec extends Specification with UsersIOTest with Users
.eventually(delay = 1.second, timeout = 30.seconds)
// when
paginations1 <- permissions.traverse(permission =>
- usersReads.userReads.paginate(User.Sorting.Newest, 0L, 1, List(User.Filter.HasPermission(permission)))
+ usersReads.userReads.paginate(User.Sorting.Newest,
+ Paginated.Offset(0L),
+ Paginated.Limit(1),
+ List(User.Filter.HasPermission(permission))
+ )
)
paginations2 <- permissions.traverse(permission =>
- usersReads.userReads.paginate(User.Sorting.Newest,
- 0L,
- 1,
- List(User.Filter.HasPermissions(Permissions(Set(permission))))
+ usersReads.userReads.paginate(
+ User.Sorting.Newest,
+ Paginated.Offset(0L),
+ Paginated.Limit(1),
+ List(User.Filter.HasPermissions(Permissions(Set(permission))))
)
)
} yield {
// then
- paginations1.map(_.entities.size) must_=== permissions.map(_ => 1)
- paginations2.map(_.entities.size) must_=== permissions.map(_ => 1)
+ paginations1.map(_.entities.size) === permissions.map(_ => 1)
+ paginations2.map(_.entities.size) === permissions.map(_ => 1)
}
}
}
diff --git a/modules/users-impl/src/it/scala/io/branchtalk/users/UserReadsWritesSpec.scala b/modules/users-impl/src/test/scala/io/branchtalk/users/UserReadsWritesSpec.scala
similarity index 83%
rename from modules/users-impl/src/it/scala/io/branchtalk/users/UserReadsWritesSpec.scala
rename to modules/users-impl/src/test/scala/io/branchtalk/users/UserReadsWritesSpec.scala
index 554c8b33..8fc8d34c 100644
--- a/modules/users-impl/src/it/scala/io/branchtalk/users/UserReadsWritesSpec.scala
+++ b/modules/users-impl/src/test/scala/io/branchtalk/users/UserReadsWritesSpec.scala
@@ -1,14 +1,13 @@
package io.branchtalk.users
import cats.effect.IO
-import io.branchtalk.shared.model.{ CommonError, ID, OptionUpdatable, TestUUIDGenerator, Updatable }
+import io.branchtalk.shared.model.*
import io.branchtalk.users.model.{ Password, Permission, Permissions, User }
-import monocle.macros.syntax.lens._
import org.specs2.mutable.Specification
-final class UserReadsWritesSpec extends Specification with UsersIOTest with UsersFixtures {
+final class UserReadsWritesSpec extends Specification, UsersIOTest, UsersFixtures {
- implicit protected val uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
+ protected given uuidGenerator: TestUUIDGenerator = new TestUUIDGenerator
"User Reads & Writes" should {
@@ -20,7 +19,7 @@ final class UserReadsWritesSpec extends Specification with UsersIOTest with User
creationData <- (0 until 3).toList.traverse(_ => userCreate)
// when
toCreate <- creationData.traverse(usersWrites.userWrites.createUser)
- ids = toCreate.map(_._1.id)
+ ids = toCreate.map(_._1.unwrap)
users <- ids.traverse(usersReads.userReads.requireById).eventually()
usersOpt <- ids.traverse(usersReads.userReads.getById).eventually()
usersExist <- ids.traverse(usersReads.userReads.exists).eventually()
@@ -37,7 +36,7 @@ final class UserReadsWritesSpec extends Specification with UsersIOTest with User
"don't update a User that doesn't exists" in {
for {
// given
- moderatorID <- userCreate.flatMap(usersWrites.userWrites.createUser).map(_._1.id)
+ moderatorID <- userCreate.flatMap(usersWrites.userWrites.createUser).map(_._1.unwrap)
_ <- usersReads.userReads.requireById(moderatorID).eventually()
creationData <- (0 until 3).toList.traverse(_ => userCreate)
fakeUpdateData <- creationData.traverse { data =>
@@ -62,11 +61,11 @@ final class UserReadsWritesSpec extends Specification with UsersIOTest with User
"update an existing User" in {
for {
// given
- moderatorID <- userCreate.flatMap(usersWrites.userWrites.createUser).map(_._1.id)
+ moderatorID <- userCreate.flatMap(usersWrites.userWrites.createUser).map(_._1.unwrap)
_ <- usersReads.userReads.requireById(moderatorID).eventually()
creationData <- (0 until 3).toList.traverse(_ => userCreate)
toCreate <- creationData.traverse(usersWrites.userWrites.createUser)
- ids = toCreate.map(_._1.id)
+ ids = toCreate.map(_._1.unwrap)
created <- ids.traverse(usersReads.userReads.requireById).eventually()
updateData = created.zipWithIndex.collect {
case (User(id, data), 0) =>
@@ -111,19 +110,15 @@ final class UserReadsWritesSpec extends Specification with UsersIOTest with User
.collect {
case ((User(id, older), User(_, newer)), 0) =>
// set case
- older.focus(_.permissions).replace(Permissions.empty.append(Permission.IsUser(id))) must_=== newer
- .focus(_.lastModifiedAt)
- .replace(None)
+ older.copy(permissions = Permissions.empty.append(Permission.IsUser(id))) === newer
+ .copy(lastModifiedAt = None)
case ((User(_, older), User(_, newer)), 1) =>
// keep case
- older must_=== newer
+ older === newer
case ((User(_, older), User(_, newer)), 2) =>
// erase case
- older
- .focus(_.permissions)
- .replace(Permissions.empty.append(Permission.ModerateUsers))
- .focus(_.description)
- .replace(None) must_=== newer.focus(_.lastModifiedAt).replace(None)
+ older.copy(permissions = Permissions.empty.append(Permission.ModerateUsers), description = None) === newer
+ .copy(lastModifiedAt = None)
}
.lastOption
.getOrElse(true must beFalse)
@@ -132,12 +127,12 @@ final class UserReadsWritesSpec extends Specification with UsersIOTest with User
"allow delete of a created User" in {
for {
// given
- moderatorID <- userCreate.flatMap(usersWrites.userWrites.createUser).map(_._1.id)
+ moderatorID <- userCreate.flatMap(usersWrites.userWrites.createUser).map(_._1.unwrap)
_ <- usersReads.userReads.requireById(moderatorID).eventually()
creationData <- (0 until 3).toList.traverse(_ => userCreate)
// when
toCreate <- creationData.traverse(usersWrites.userWrites.createUser)
- ids = toCreate.map(_._1.id)
+ ids = toCreate.map(_._1.unwrap)
_ <- ids.traverse(usersReads.userReads.requireById).eventually()
_ <- ids.map(User.Delete(_, moderatorID.some)).traverse(usersWrites.userWrites.deleteUser)
_ <- ids
@@ -157,12 +152,12 @@ final class UserReadsWritesSpec extends Specification with UsersIOTest with User
for {
// given
goodPassword <- passwordCreate("password")
- rawGoodPassword <- Password.Raw.parse[IO]("password".getBytes)
- rawBadPassword <- Password.Raw.parse[IO]("bad".getBytes)
+ rawGoodPassword <- ParseNewtype[IO].parse[Password.Raw]("password".getBytes)
+ rawBadPassword <- ParseNewtype[IO].parse[Password.Raw]("bad".getBytes)
userId <- userCreate
.map(_.copy(password = goodPassword))
.flatMap(usersWrites.userWrites.createUser)
- .map(_._1.id)
+ .map(_._1.unwrap)
user <- usersReads.userReads.requireById(userId).eventually()
// when
ok <- usersReads.userReads.authenticate(user.data.username, rawGoodPassword).attempt
@@ -170,7 +165,7 @@ final class UserReadsWritesSpec extends Specification with UsersIOTest with User
} yield {
// then
ok must beRight(user)
- fail must beLeft(anInstanceOf[CommonError.InvalidCredentials])
+ fail must beLeft(beAnInstanceOf[CommonError.InvalidCredentials])
}
}
}
diff --git a/modules/users-impl/src/it/scala/io/branchtalk/users/UsersFixtures.scala b/modules/users-impl/src/test/scala/io/branchtalk/users/UsersFixtures.scala
similarity index 54%
rename from modules/users-impl/src/it/scala/io/branchtalk/users/UsersFixtures.scala
rename to modules/users-impl/src/test/scala/io/branchtalk/users/UsersFixtures.scala
index e3d7fe06..fe1e2ac7 100644
--- a/modules/users-impl/src/it/scala/io/branchtalk/users/UsersFixtures.scala
+++ b/modules/users-impl/src/test/scala/io/branchtalk/users/UsersFixtures.scala
@@ -1,29 +1,29 @@
package io.branchtalk.users
import cats.effect.{ Clock, IO }
-import io.branchtalk.shared.model._
+import io.branchtalk.shared.model.*
import io.branchtalk.users.model.{ Ban, Channel, Password, Session, User }
-import io.branchtalk.shared.Fixtures._
+import io.branchtalk.shared.Fixtures.*
import scala.util.Random
trait UsersFixtures {
- def channelIDCreate(implicit uuidGenerator: UUIDGenerator): IO[ID[Channel]] =
+ def channelIDCreate(using UUID.Generator): IO[ID[Channel]] =
ID.create[IO, Channel]
def passwordCreate(password: String = "pass"): IO[Password] =
- Password.Raw.parse[IO](password.getBytes).map(Password.create)
+ ParseNewtype[IO].parse[Password.Raw](password.getBytes).map(Password.create)
def userCreate: IO[User.Create] =
(
- company().map(_.getEmail).map(e => s"${Random.nextLong()}+$e").flatMap(User.Email.parse[IO]),
- textProducer.map(_.randomString(10)).flatMap(User.Name.parse[IO]),
- textProducer.map(_.loremIpsum()).map(User.Description(_).some),
+ company().map(_.getEmail).map(e => s"${Random.nextLong()}+$e").flatMap(ParseNewtype[IO].parse[User.Email](_)),
+ textProducer.map(_.randomString(10)).flatMap(ParseNewtype[IO].parse[User.Name](_)),
+ textProducer.map(_.loremIpsum()).flatMap(ParseNewtype[IO].parse[User.Description](_)).map(_.some),
passwordCreate()
).mapN(User.Create.apply)
- def sessionCreate(userID: ID[User])(implicit clock: Clock[IO]): IO[Session.Create] =
+ def sessionCreate(userID: ID[User])(using Clock[IO]): IO[Session.Create] =
(
userID.pure[IO],
(Session.Usage.UserSession: Session.Usage).pure[IO],
@@ -33,7 +33,7 @@ trait UsersFixtures {
def banCreate(userID: ID[User], channelID: ID[Channel]): IO[Ban] =
(
userID.pure[IO],
- textProducer.map(_.loremIpsum()).flatMap(Ban.Reason.parse[IO]),
+ textProducer.map(_.loremIpsum()).flatMap(ParseNewtype[IO].parse[Ban.Reason](_)),
Ban.Scope.ForChannel(channelID).pure[IO]
).mapN(Ban.apply _)
}
diff --git a/modules/users-impl/src/it/scala/io/branchtalk/users/UsersIOTest.scala b/modules/users-impl/src/test/scala/io/branchtalk/users/UsersIOTest.scala
similarity index 61%
rename from modules/users-impl/src/it/scala/io/branchtalk/users/UsersIOTest.scala
rename to modules/users-impl/src/test/scala/io/branchtalk/users/UsersIOTest.scala
index f5e0550b..d3ba4adc 100644
--- a/modules/users-impl/src/it/scala/io/branchtalk/users/UsersIOTest.scala
+++ b/modules/users-impl/src/test/scala/io/branchtalk/users/UsersIOTest.scala
@@ -1,18 +1,18 @@
package io.branchtalk.users
import cats.effect.{ IO, Resource }
-import io.branchtalk.shared.infrastructure.DomainConfig
+import io.branchtalk.shared.infrastructure.*
import io.branchtalk.{ IOTest, ResourcefulTest }
-import io.branchtalk.shared.model.UUIDGenerator
+import io.branchtalk.shared.model.UUID
-trait UsersIOTest extends IOTest with ResourcefulTest {
+trait UsersIOTest extends IOTest, ResourcefulTest {
- implicit protected def uuidGenerator: UUIDGenerator
+ protected given uuidGenerator: UUID.Generator
// populated by resources
- protected var usersCfg: DomainConfig = _
- protected var usersReads: UsersReads[IO] = _
- protected var usersWrites: UsersWrites[IO] = _
+ protected var usersCfg: DomainModule.Config = _
+ protected var usersReads: UsersReads[IO] = _
+ protected var usersWrites: UsersWrites[IO] = _
protected lazy val usersResource: Resource[IO, Unit] = for {
_ <- TestUsersConfig.loadDomainConfig[IO].map(usersCfg = _)
diff --git a/modules/users/src/main/scala/io/branchtalk/users/events/BanEvent.scala b/modules/users/src/main/scala/io/branchtalk/users/events/BanEvent.scala
index 6a0801fe..5666c8a1 100644
--- a/modules/users/src/main/scala/io/branchtalk/users/events/BanEvent.scala
+++ b/modules/users/src/main/scala/io/branchtalk/users/events/BanEvent.scala
@@ -1,30 +1,28 @@
package io.branchtalk.users.events
import com.sksamuel.avro4s.{ Decoder, Encoder, SchemaFor }
-import io.branchtalk.ADT
-import io.branchtalk.logging.CorrelationID
-import io.branchtalk.shared.model._
-import io.branchtalk.shared.model.AvroSupport._
+import io.branchtalk.logging.*
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.model.AvroSupport.{ *, given }
import io.branchtalk.users.model.{ Ban, User }
-import io.scalaland.catnip.Semi
-@Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) sealed trait BanEvent extends ADT
+sealed trait BanEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
object BanEvent {
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Banned(
+ final case class Banned(
bannedUserID: ID[User],
moderatorID: Option[ID[User]],
scope: Ban.Scope,
reason: Ban.Reason,
createdAt: CreationTime,
correlationID: CorrelationID
- ) extends BanEvent
+ ) extends BanEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Unbanned(
+ final case class Unbanned(
bannedUserID: ID[User],
moderatorID: Option[ID[User]],
scope: Ban.Scope,
modifiedAt: ModificationTime,
correlationID: CorrelationID
- ) extends BanEvent
+ ) extends BanEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
}
diff --git a/modules/users/src/main/scala/io/branchtalk/users/events/SessionEvent.scala b/modules/users/src/main/scala/io/branchtalk/users/events/SessionEvent.scala
index a71bb694..a2c5053c 100644
--- a/modules/users/src/main/scala/io/branchtalk/users/events/SessionEvent.scala
+++ b/modules/users/src/main/scala/io/branchtalk/users/events/SessionEvent.scala
@@ -1,26 +1,24 @@
package io.branchtalk.users.events
-import com.sksamuel.avro4s._
-import io.branchtalk.ADT
-import io.branchtalk.logging.CorrelationID
-import io.branchtalk.shared.model._
-import io.branchtalk.shared.model.AvroSupport._
+import com.sksamuel.avro4s.*
+import io.branchtalk.logging.*
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.model.AvroSupport.{ *, given }
import io.branchtalk.users.model.{ Session, User }
-import io.scalaland.catnip.Semi
-@Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) sealed trait SessionEvent extends ADT
+sealed trait SessionEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
object SessionEvent {
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class LoggedIn(
+ final case class LoggedIn(
id: ID[Session],
userID: ID[User],
expiresAt: Session.ExpirationTime,
correlationID: CorrelationID
- ) extends SessionEvent
+ ) extends SessionEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class LoggedOut(
+ final case class LoggedOut(
id: ID[Session],
userID: ID[User],
correlationID: CorrelationID
- ) extends SessionEvent
+ ) extends SessionEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
}
diff --git a/modules/users/src/main/scala/io/branchtalk/users/events/UserEvent.scala b/modules/users/src/main/scala/io/branchtalk/users/events/UserEvent.scala
index aac70009..72a528aa 100644
--- a/modules/users/src/main/scala/io/branchtalk/users/events/UserEvent.scala
+++ b/modules/users/src/main/scala/io/branchtalk/users/events/UserEvent.scala
@@ -1,20 +1,19 @@
package io.branchtalk.users.events
-import com.sksamuel.avro4s._
-import io.branchtalk.ADT
-import io.branchtalk.logging.CorrelationID
-import io.branchtalk.shared.model._
-import io.branchtalk.shared.model.AvroSupport._
+import com.sksamuel.avro4s.*
+import io.branchtalk.logging.*
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.model.AvroSupport.{ *, given }
import io.branchtalk.shared.model.AvroSerialization.DeserializationResult
import io.branchtalk.users.model.{ Password, Permission, Session, User }
-import io.scalaland.catnip.Semi
-import io.scalaland.chimney.dsl._
+import io.scalaland.chimney.dsl.*
+import io.scalaland.chimney.partial.syntax.*
// user events doesn't store any data as they can be sensitive data
-@Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) sealed trait UserEvent extends ADT
+sealed trait UserEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
object UserEvent {
- @Semi(FastEq, ShowPretty) final case class Created(
+ final case class Created(
id: ID[User],
sessionID: ID[Session], // session created by registration
email: SensitiveData[User.Email],
@@ -24,7 +23,8 @@ object UserEvent {
sessionExpiresAt: Session.ExpirationTime,
createdAt: CreationTime,
correlationID: CorrelationID
- ) {
+ ) derives FastEq,
+ ShowPretty {
def encrypt(
algorithm: SensitiveData.Algorithm,
@@ -38,7 +38,7 @@ object UserEvent {
}
object Created {
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Encrypted(
+ final case class Encrypted(
id: ID[User],
sessionID: ID[Session], // session created by registration
email: SensitiveData.Encrypted[User.Email],
@@ -48,21 +48,23 @@ object UserEvent {
sessionExpiresAt: Session.ExpirationTime,
createdAt: CreationTime,
correlationID: CorrelationID
- ) extends UserEvent {
+ ) extends UserEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor {
def decrypt(
algorithm: SensitiveData.Algorithm,
key: SensitiveData.Key
): DeserializationResult[Created] = this
- .intoF[DeserializationResult, Created]
- .withFieldComputedF(_.email, _.email.decrypt(algorithm, key))
- .withFieldComputedF(_.username, _.username.decrypt(algorithm, key))
- .withFieldComputedF(_.password, _.password.decrypt(algorithm, key))
+ .intoPartial[Created]
+ .withFieldComputedPartial(_.email, _.email.decrypt(algorithm, key).asResult)
+ .withFieldComputedPartial(_.username, _.username.decrypt(algorithm, key).asResult)
+ .withFieldComputedPartial(_.password, _.password.decrypt(algorithm, key).asResult)
.transform
+ .asEither
+ .leftMap(e => DeserializationError.DecryptionError(this.show, e.asErrorPathMessageStrings.toMap))
}
}
- @Semi(FastEq, ShowPretty) final case class Updated(
+ final case class Updated(
id: ID[User],
moderatorID: Option[ID[User]],
newUsername: Updatable[SensitiveData[User.Name]],
@@ -71,7 +73,8 @@ object UserEvent {
updatePermissions: List[Permission.Update],
modifiedAt: ModificationTime,
correlationID: CorrelationID
- ) {
+ ) derives FastEq,
+ ShowPretty {
def encrypt(
algorithm: SensitiveData.Algorithm,
@@ -84,7 +87,7 @@ object UserEvent {
}
object Updated {
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Encrypted(
+ final case class Encrypted(
id: ID[User],
moderatorID: Option[ID[User]],
newUsername: Updatable[SensitiveData.Encrypted[User.Name]],
@@ -93,23 +96,25 @@ object UserEvent {
updatePermissions: List[Permission.Update],
modifiedAt: ModificationTime,
correlationID: CorrelationID
- ) extends UserEvent {
+ ) extends UserEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor {
def decrypt(
algorithm: SensitiveData.Algorithm,
key: SensitiveData.Key
): DeserializationResult[Updated] = this
- .intoF[DeserializationResult, Updated]
- .withFieldComputedF(_.newUsername, _.newUsername.traverse(_.decrypt(algorithm, key)))
- .withFieldComputedF(_.newPassword, _.newPassword.traverse(_.decrypt(algorithm, key)))
+ .intoPartial[Updated]
+ .withFieldComputedPartial(_.newUsername, _.newUsername.traverse(_.decrypt(algorithm, key)).asResult)
+ .withFieldComputedPartial(_.newPassword, _.newPassword.traverse(_.decrypt(algorithm, key)).asResult)
.transform
+ .asEither
+ .leftMap(e => DeserializationError.DecryptionError(this.show, e.asErrorPathMessageStrings.toMap))
}
}
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) final case class Deleted(
+ final case class Deleted(
id: ID[User],
moderatorID: Option[ID[User]],
deletedAt: ModificationTime,
correlationID: CorrelationID
- ) extends UserEvent
+ ) extends UserEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
}
diff --git a/modules/users/src/main/scala/io/branchtalk/users/events/UsersEvent.scala b/modules/users/src/main/scala/io/branchtalk/users/events/UsersEvent.scala
index bedd4b44..134a7b6c 100644
--- a/modules/users/src/main/scala/io/branchtalk/users/events/UsersEvent.scala
+++ b/modules/users/src/main/scala/io/branchtalk/users/events/UsersEvent.scala
@@ -1,20 +1,24 @@
package io.branchtalk.users.events
-import com.sksamuel.avro4s._
-import io.branchtalk.shared.model._
-import io.branchtalk.shared.model.AvroSupport._
-import io.branchtalk.ADT
-import io.scalaland.catnip.Semi
+import com.sksamuel.avro4s.*
+import io.branchtalk.shared.model.*
+import io.branchtalk.shared.model.AvroSupport.{ *, given }
-@Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor) sealed trait UsersEvent extends ADT
+sealed trait UsersEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
object UsersEvent {
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor)
- final case class ForUser(user: UserEvent) extends UsersEvent
+ final case class ForUser(
+ user: UserEvent
+ ) extends UsersEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor)
final case class ForSession(session: SessionEvent) extends UsersEvent
+ derives Decoder,
+ Encoder,
+ FastEq,
+ ShowPretty,
+ SchemaFor
- @Semi(Decoder, Encoder, FastEq, ShowPretty, SchemaFor)
- final case class ForBan(ban: BanEvent) extends UsersEvent
+ final case class ForBan(
+ ban: BanEvent
+ ) extends UsersEvent derives Decoder, Encoder, FastEq, ShowPretty, SchemaFor
}
diff --git a/modules/users/src/main/scala/io/branchtalk/users/model/Ban.scala b/modules/users/src/main/scala/io/branchtalk/users/model/Ban.scala
index 0271a7c2..186fe02c 100644
--- a/modules/users/src/main/scala/io/branchtalk/users/model/Ban.scala
+++ b/modules/users/src/main/scala/io/branchtalk/users/model/Ban.scala
@@ -1,6 +1,70 @@
package io.branchtalk.users.model
-import io.branchtalk.shared.model.ID
+import cats.Show
+import cats.effect.Sync
+import enumeratum.*
+import enumeratum.EnumEntry.Hyphencase
+import io.branchtalk.shared.model.*
-final case class Ban(bannedUserID: ID[User], reason: Ban.Reason, scope: Ban.Scope)
-object Ban extends BanProperties with BanCommands
+final case class Ban(
+ bannedUserID: ID[User],
+ reason: Ban.Reason,
+ scope: Ban.Scope
+) derives FastEq,
+ ShowPretty
+object Ban {
+
+ final case class Order(
+ bannedUserID: ID[User],
+ reason: Ban.Reason,
+ scope: Ban.Scope,
+ moderatorID: Option[ID[User]]
+ ) derives FastEq,
+ ShowPretty
+
+ final case class Lift(
+ bannedUserID: ID[User],
+ scope: Ban.Scope,
+ moderatorID: Option[ID[User]]
+ ) derives FastEq,
+ ShowPretty
+
+ type Reason = Reason.Type
+ object Reason extends Newtype[String] {
+
+ override inline def validate(input: String): Boolean = input.nonEmpty
+
+ def unapply(reason: Reason): Some[String] = Some(reason.unwrap)
+
+ given Show[Reason] = unsafeMakeF[Show](Show[String])
+ given cats.Order[Reason] = unsafeMakeF[cats.Order](cats.Order[String])
+ }
+
+ enum Scope derives FastEq, ShowPretty {
+ case ForChannel(channelID: ID[Channel])
+ case Globally
+ }
+ object Scope {
+
+ enum Type extends EnumEntry with Hyphencase derives FastEq, ShowPretty {
+ case ForChannel
+ case Globally
+ }
+
+ object Tupled {
+ @SuppressWarnings(Array("org.wartremover.warts.Throw")) // illegal input from the DB
+ def apply(scopeType: Type, scopeValue: Option[UUID]): Scope = (scopeType, scopeValue) match {
+ case (Type.ForChannel, Some(uuid)) => Scope.ForChannel(ID[Channel](uuid))
+ case (Type.Globally, _) => Scope.Globally
+ case _ => throw new IllegalArgumentException("Expected ID for non-Global Scope")
+ }
+
+ def unpack(scope: Scope): (Type, Option[UUID]) = scope match {
+ case ForChannel(channelID) => (Type.ForChannel, channelID.unwrap.some)
+ case Globally => (Type.Globally, none)
+ }
+
+ def unapply(scope: Scope): Some[(Type, Option[UUID])] = Some(unpack(scope))
+ }
+ }
+}
diff --git a/modules/users/src/main/scala/io/branchtalk/users/model/BanCommands.scala b/modules/users/src/main/scala/io/branchtalk/users/model/BanCommands.scala
deleted file mode 100644
index 6726ce68..00000000
--- a/modules/users/src/main/scala/io/branchtalk/users/model/BanCommands.scala
+++ /dev/null
@@ -1,26 +0,0 @@
-package io.branchtalk.users.model
-
-import io.branchtalk.shared.model.{ FastEq, ID, ShowPretty }
-import io.scalaland.catnip.Semi
-
-trait BanCommands {
- type Order = BanCommands.Order
- type Lift = BanCommands.Lift
- val Order = BanCommands.Order
- val Lift = BanCommands.Lift
-}
-object BanCommands {
-
- @Semi(FastEq, ShowPretty) final case class Order(
- bannedUserID: ID[User],
- reason: Ban.Reason,
- scope: Ban.Scope,
- moderatorID: Option[ID[User]]
- )
-
- @Semi(FastEq, ShowPretty) final case class Lift(
- bannedUserID: ID[User],
- scope: Ban.Scope,
- moderatorID: Option[ID[User]]
- )
-}
diff --git a/modules/users/src/main/scala/io/branchtalk/users/model/BanProperties.scala b/modules/users/src/main/scala/io/branchtalk/users/model/BanProperties.scala
deleted file mode 100644
index 5962e194..00000000
--- a/modules/users/src/main/scala/io/branchtalk/users/model/BanProperties.scala
+++ /dev/null
@@ -1,60 +0,0 @@
-package io.branchtalk.users.model
-
-import cats.{ Order, Show }
-import cats.effect.Sync
-import enumeratum._
-import enumeratum.EnumEntry.Hyphencase
-import eu.timepit.refined.collection.NonEmpty
-import eu.timepit.refined.types.string.NonEmptyString
-import io.branchtalk.ADT
-import io.branchtalk.shared.model._
-import io.estatico.newtype.macros.newtype
-import io.scalaland.catnip.Semi
-
-trait BanProperties {
- type Reason = BanProperties.Reason
- type Scope = BanProperties.Scope
- val Reason = BanProperties.Reason
- val Scope = BanProperties.Scope
-}
-object BanProperties {
-
- @newtype final case class Reason(nonEmptyString: NonEmptyString)
- object Reason {
- def unapply(reason: Reason): Some[NonEmptyString] = Some(reason.nonEmptyString)
- def parse[F[_]: Sync](string: String): F[Reason] = ParseRefined[F].parse[NonEmpty](string).map(Reason.apply)
-
- implicit val show: Show[Reason] = Show.wrap(_.nonEmptyString.value)
- implicit val order: Order[Reason] = Order.by(_.nonEmptyString.value)
- }
-
- @Semi(FastEq, ShowPretty) sealed trait Scope extends ADT
- object Scope {
- final case class ForChannel(channelID: ID[Channel]) extends Scope
- case object Globally extends Scope
-
- @Semi(FastEq, ShowPretty) sealed trait Type extends EnumEntry with Hyphencase
- object Type extends Enum[Type] {
- case object ForChannel extends Type
- case object Globally extends Type
-
- val values: IndexedSeq[Type] = findValues
- }
-
- object Tupled {
- @SuppressWarnings(Array("org.wartremover.warts.Throw")) // illegal input from the DB
- def apply(scopeType: Type, scopeValue: Option[UUID]): Scope = (scopeType, scopeValue) match {
- case (Type.ForChannel, Some(uuid)) => Scope.ForChannel(ID[Channel](uuid))
- case (Type.Globally, _) => Scope.Globally
- case _ => throw new IllegalArgumentException("Expected ID for non-Global Scope")
- }
-
- def unpack(scope: Scope): (Type, Option[UUID]) = scope match {
- case ForChannel(channelID) => (Type.ForChannel, channelID.uuid.some)
- case Globally => (Type.Globally, none)
- }
-
- def unapply(scope: Scope): Some[(Type, Option[UUID])] = Some(unpack(scope))
- }
- }
-}
diff --git a/modules/users/src/main/scala/io/branchtalk/users/model/Password.scala b/modules/users/src/main/scala/io/branchtalk/users/model/Password.scala
index e36510f1..3ce2b615 100644
--- a/modules/users/src/main/scala/io/branchtalk/users/model/Password.scala
+++ b/modules/users/src/main/scala/io/branchtalk/users/model/Password.scala
@@ -6,17 +6,14 @@ import cats.{ Eq, Show }
import cats.effect.{ Sync, SyncIO }
import enumeratum.{ Enum, EnumEntry }
import enumeratum.EnumEntry.Hyphencase
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.collection.NonEmpty
-import io.branchtalk.shared.model._
-import io.estatico.newtype.macros.newtype
-import io.scalaland.catnip.Semi
+import io.branchtalk.shared.model.*
-@Semi(FastEq, ShowPretty) final case class Password(
+final case class Password(
algorithm: Password.Algorithm,
hash: Password.Hash,
salt: Password.Salt
-) {
+) derives FastEq,
+ ShowPretty {
def update(raw: Password.Raw): Password = copy(hash = algorithm.hashRaw(raw, salt))
def verify(raw: Password.Raw): Boolean = algorithm.verify(raw, salt, hash)
@@ -24,20 +21,18 @@ import io.scalaland.catnip.Semi
// allows comparison of Passwords which would otherwise use Array's hashCode method
override def equals(other: Any): Boolean = other match {
- case Password(`algorithm`, otherHash, otherSalt)
- if hash.bytes.sameElements(otherHash.bytes) && salt.bytes.sameElements(otherSalt.bytes) =>
- true
- case _ => false
+ case Password(`algorithm`, otherHash, otherSalt) => hash === otherHash && salt === otherSalt
+ case _ => false
}
- override def hashCode(): Int = algorithm.hashCode() ^ hash.bytes.toSeq.hashCode() ^ salt.bytes.toSeq.hashCode()
+ override def hashCode(): Int = algorithm.hashCode() ^ hash.unwrap.toSeq.hashCode() ^ salt.unwrap.toSeq.hashCode()
}
object Password {
- @Semi(FastEq, ShowPretty) sealed trait Algorithm extends EnumEntry with Hyphencase {
+ sealed trait Algorithm extends EnumEntry with Hyphencase derives FastEq, ShowPretty {
- def createSalt: Password.Salt
- def hashRaw(raw: Password.Raw, salt: Password.Salt): Password.Hash
+ def createSalt: Password.Salt
+ def hashRaw(raw: Password.Raw, salt: Password.Salt): Password.Hash
def verify(raw: Password.Raw, salt: Password.Salt, hash: Password.Hash): Boolean
}
object Algorithm extends Enum[Algorithm] {
@@ -58,10 +53,10 @@ object Password {
}
override def hashRaw(raw: Password.Raw, salt: Password.Salt): Password.Hash =
- Password.Hash(hasher.hashRaw(cost, salt.bytes, raw.nonEmptyBytes).rawHash)
+ Password.Hash(hasher.hashRaw(cost, salt.unwrap, raw.unwrap).rawHash)
override def verify(raw: Password.Raw, salt: Password.Salt, hash: Password.Hash): Boolean =
- verifier.verify(raw.nonEmptyBytes, cost, salt.bytes, hash.bytes).verified
+ verifier.verify(raw.unwrap, cost, salt.unwrap, hash.unwrap).verified
}
def default: Algorithm = BCrypt
@@ -69,33 +64,34 @@ object Password {
val values: IndexedSeq[Algorithm] = findValues
}
- @newtype final case class Hash(bytes: Array[Byte])
- object Hash {
- def unapply(hash: Hash): Some[Array[Byte]] = Some(hash.bytes)
+ private val arrayEq: Eq[Array[Byte]] = _ sameElements _
- implicit val show: Show[Hash] = Show.wrap(_ => "EDITED OUT")
- implicit val eq: Eq[Hash] = _.bytes sameElements _.bytes
+ type Hash = Hash.Type
+ object Hash extends Newtype[Array[Byte]] {
+ def unapply(hash: Hash): Some[Array[Byte]] = Some(hash.unwrap)
+
+ given Show[Hash] = _ => "EDITED OUT"
+ given Eq[Hash] = unsafeMakeF[Eq](arrayEq)
}
- @newtype final case class Salt(bytes: Array[Byte])
- object Salt {
- def unapply(salt: Salt): Some[Array[Byte]] = Some(salt.bytes)
+ type Salt = Salt.Type
+ object Salt extends Newtype[Array[Byte]] {
+ def unapply(salt: Salt): Some[Array[Byte]] = Some(salt.unwrap)
- implicit val show: Show[Salt] = Show.wrap(_ => "EDITED OUT")
- implicit val eq: Eq[Salt] = _.bytes sameElements _.bytes
+ given Show[Salt] = _ => "EDITED OUT"
+ given Eq[Salt] = unsafeMakeF[Eq](arrayEq)
}
- @newtype final case class Raw(nonEmptyBytes: Array[Byte] Refined NonEmpty)
- object Raw {
- def unapply(raw: Raw): Some[Array[Byte] Refined NonEmpty] = Some(raw.nonEmptyBytes)
- def parse[F[_]: Sync](bytes: Array[Byte]): F[Raw] =
- ParseRefined[F].parse[NonEmpty](bytes).map(Raw.apply)
+ type Raw = Raw.Type
+ object Raw extends Newtype[Array[Byte]] {
+ override inline def validate(input: Array[Byte]): Boolean = input.nonEmpty
+
+ def unapply(hash: Raw): Some[Array[Byte]] = Some(hash.unwrap)
- def fromString(string: String Refined NonEmpty): Raw =
- Raw(ParseRefined[SyncIO].parse[NonEmpty](string.getBytes(branchtalkCharset)).unsafeRunSync())
+ def fromString(string: String): Either[String, Raw] = make(string.getBytes(branchtalkCharset))
- implicit val show: Show[Raw] = Show.wrap(_ => "EDITED OUT")
- implicit val eq: Eq[Raw] = _.nonEmptyBytes.value sameElements _.nonEmptyBytes.value
+ given Show[Raw] = _ => "EDITED OUT"
+ given Eq[Raw] = unsafeMakeF[Eq](arrayEq)
}
def create(raw: Password.Raw): Password = {
diff --git a/modules/users/src/main/scala/io/branchtalk/users/model/Permission.scala b/modules/users/src/main/scala/io/branchtalk/users/model/Permission.scala
index ad276c99..d768a3f9 100644
--- a/modules/users/src/main/scala/io/branchtalk/users/model/Permission.scala
+++ b/modules/users/src/main/scala/io/branchtalk/users/model/Permission.scala
@@ -1,20 +1,23 @@
package io.branchtalk.users.model
import cats.Order
-import io.branchtalk.ADT
-import io.branchtalk.shared.model.{ ID, ShowPretty }
-import io.scalaland.catnip.Semi
+import io.branchtalk.shared.model.*
-@Semi(ShowPretty) sealed trait Permission extends ADT
-object Permission extends PermissionCommands {
+enum Permission derives ShowPretty {
+ case Administrate
+ case IsUser(userID: ID[User])
+ case ModerateUsers
+ case ModerateChannel(channelID: ID[Channel])
+ case CanPublish(channelID: ID[Channel])
+}
+object Permission {
- case object Administrate extends Permission
- final case class IsUser(userID: ID[User]) extends Permission
- case object ModerateUsers extends Permission
- final case class ModerateChannel(channelID: ID[Channel]) extends Permission
- final case class CanPublish(channelID: ID[Channel]) extends Permission
+ enum Update derives FastEq, ShowPretty {
+ case Add(permission: Permission)
+ case Remove(permission: Permission)
+ }
- implicit val order: Order[Permission] = {
+ given Order[Permission] = {
case (Administrate, Administrate) => 0
case (Administrate, _) => 1
case (IsUser(u1), IsUser(u2)) => Order[ID[User]].compare(u1, u2)
diff --git a/modules/users/src/main/scala/io/branchtalk/users/model/PermissionCommands.scala b/modules/users/src/main/scala/io/branchtalk/users/model/PermissionCommands.scala
deleted file mode 100644
index 8b99aca7..00000000
--- a/modules/users/src/main/scala/io/branchtalk/users/model/PermissionCommands.scala
+++ /dev/null
@@ -1,18 +0,0 @@
-package io.branchtalk.users.model
-
-import io.branchtalk.ADT
-import io.branchtalk.shared.model.{ FastEq, ShowPretty }
-import io.scalaland.catnip.Semi
-
-trait PermissionCommands {
- type Update = PermissionCommands.Update
- val Update = PermissionCommands.Update
-}
-object PermissionCommands {
-
- @Semi(FastEq, ShowPretty) sealed trait Update extends ADT
- object Update {
- final case class Add(permission: Permission) extends Update
- final case class Remove(permission: Permission) extends Update
- }
-}
diff --git a/modules/users/src/main/scala/io/branchtalk/users/model/Permissions.scala b/modules/users/src/main/scala/io/branchtalk/users/model/Permissions.scala
new file mode 100644
index 00000000..4d680ccf
--- /dev/null
+++ b/modules/users/src/main/scala/io/branchtalk/users/model/Permissions.scala
@@ -0,0 +1,36 @@
+package io.branchtalk.users.model
+
+import cats.{ Eq, Eval, Show }
+
+type Permissions = Permissions.Type
+object Permissions extends Newtype[Set[Permission]] {
+ def unapply(permissions: Permissions): Some[Set[Permission]] = Some(permissions.unwrap)
+
+ def empty: Permissions = Permissions(Set.empty)
+
+ @SuppressWarnings(Array("org.wartremover.warts.All")) // Eval should be stack-safe
+ def validatePermissions(required: RequiredPermissions, existing: Permissions): Boolean = {
+ def permitted(permission: Permission) = existing.unwrap.contains(permission)
+ def evaluate(req: RequiredPermissions): Eval[Boolean] = Eval.later(req).flatMap {
+ case RequiredPermissions.Empty => Eval.True
+ case RequiredPermissions.AllOf(set) => Eval.later(set.forall(permitted))
+ case RequiredPermissions.AnyOf(set) => Eval.later(set.exists(permitted))
+ case RequiredPermissions.And(x, y) => (evaluate(x), evaluate(y)).mapN(_ && _)
+ case RequiredPermissions.Or(x, y) => (evaluate(x), evaluate(y)).mapN(_ || _)
+ case RequiredPermissions.Not(x) => evaluate(x).map(!_)
+ }
+ evaluate(required).value
+ }
+
+ extension (permissions: Permissions) {
+
+ def append(permission: Permission): Permissions = unsafeMake(permissions.unwrap + permission)
+ def remove(permission: Permission): Permissions = unsafeMake(permissions.unwrap - permission)
+
+ def allow(required: RequiredPermissions): Boolean = validatePermissions(required, permissions)
+ def intersect(another: Permissions): Permissions = unsafeMake(permissions.unwrap intersect another.unwrap)
+ }
+
+ given Show[Permissions] = unsafeMakeF[Show](Show[Set[Permission]])
+ given Eq[Permissions] = unsafeMakeF[Eq](Eq[Set[Permission]])
+}
diff --git a/modules/users/src/main/scala/io/branchtalk/users/model/RequiredPermissions.scala b/modules/users/src/main/scala/io/branchtalk/users/model/RequiredPermissions.scala
index f04ae509..7d704083 100644
--- a/modules/users/src/main/scala/io/branchtalk/users/model/RequiredPermissions.scala
+++ b/modules/users/src/main/scala/io/branchtalk/users/model/RequiredPermissions.scala
@@ -2,51 +2,49 @@ package io.branchtalk.users.model
import cats.Eq
import cats.data.{ NonEmptyList, NonEmptySet }
-import io.branchtalk.ADT
-import io.branchtalk.shared.model.{ FastEq, ShowPretty }
-import io.scalaland.catnip.Semi
+import io.branchtalk.shared.model.*
-@Semi(FastEq, ShowPretty) sealed trait RequiredPermissions extends ADT {
+import scala.annotation.targetName
- // scalastyle:off method.name
- def &&(other: RequiredPermissions): RequiredPermissions = RequiredPermissions.And(this, other)
- def ||(other: RequiredPermissions): RequiredPermissions = RequiredPermissions.Or(this, other)
- def unary_! : RequiredPermissions = RequiredPermissions.Not(this)
- // scalastyle:on method.name
+enum RequiredPermissions derives FastEq, ShowPretty {
+ case Empty
+
+ case AllOf(toSet: NonEmptySet[Permission])
+ case AnyOf(toSet: NonEmptySet[Permission])
+
+ case And(x: RequiredPermissions, y: RequiredPermissions)
+ case Or(x: RequiredPermissions, y: RequiredPermissions)
+ case Not(x: RequiredPermissions)
+
+ @targetName("and") def &&(other: RequiredPermissions): RequiredPermissions = And(this, other)
+ @targetName("or") def ||(other: RequiredPermissions): RequiredPermissions = Or(this, other)
+ @targetName("not") def unary_! : RequiredPermissions = Not(this)
}
object RequiredPermissions {
- def empty: RequiredPermissions = Empty
- def one(permission: Permission): RequiredPermissions = AllOf(NonEmptySet.one(permission))
+ def empty: RequiredPermissions = Empty
+ def one(permission: Permission): RequiredPermissions = AllOf(NonEmptySet.one(permission))
def allOf(head: Permission, tail: Permission*): RequiredPermissions = AllOf(NonEmptySet.of(head, tail: _*))
def anyOf(head: Permission, tail: Permission*): RequiredPermissions = AnyOf(NonEmptySet.of(head, tail: _*))
- case object Empty extends RequiredPermissions
-
- final case class AllOf(toSet: NonEmptySet[Permission]) extends RequiredPermissions
- final case class AnyOf(toSet: NonEmptySet[Permission]) extends RequiredPermissions
-
- final case class And(x: RequiredPermissions, y: RequiredPermissions) extends RequiredPermissions
- final case class Or(x: RequiredPermissions, y: RequiredPermissions) extends RequiredPermissions
- final case class Not(x: RequiredPermissions) extends RequiredPermissions
-
- implicit val nesEq: Eq[NonEmptySet[Permission]] = (x: NonEmptySet[Permission], y: NonEmptySet[Permission]) =>
+ given Eq[NonEmptySet[Permission]] = (x: NonEmptySet[Permission], y: NonEmptySet[Permission]) =>
x.toSortedSet === y.toSortedSet
- implicit val nesShow: ShowPretty[NonEmptySet[Permission]] =
- (t: NonEmptySet[Permission], sb: StringBuilder, indentWith: String, indentLevel: Int) => {
+ @SuppressWarnings(Array("org.wartremover.warts.MutableDataStructures"))
+ given ShowPretty[NonEmptySet[Permission]] = {
+ (t: NonEmptySet[Permission], sb: StringBuilder, indentWith: String, indentLevel: Int) =>
val nextIndent = indentLevel + 1
- sb.append(indentWith * indentLevel).append("NonEmptySet(\n")
+ void(sb.append(indentWith * indentLevel).append("NonEmptySet(\n"))
t.toNonEmptyList match {
case NonEmptyList(head, tail) =>
sb.append(indentWith * nextIndent)
- implicitly[ShowPretty[Permission]].showPretty(head, sb, indentWith, nextIndent)
+ void(summon[ShowPretty[Permission]].showPretty(head, sb, indentWith, nextIndent))
tail.foreach { elem =>
sb.append(",\n")
sb.append(indentWith * nextIndent)
- implicitly[ShowPretty[Permission]].showPretty(elem, sb, indentWith, nextIndent)
+ summon[ShowPretty[Permission]].showPretty(elem, sb, indentWith, nextIndent)
}
sb.append("\n)")
}
sb
- }
+ }
}
diff --git a/modules/users/src/main/scala/io/branchtalk/users/model/Session.scala b/modules/users/src/main/scala/io/branchtalk/users/model/Session.scala
index d7f17888..27461132 100644
--- a/modules/users/src/main/scala/io/branchtalk/users/model/Session.scala
+++ b/modules/users/src/main/scala/io/branchtalk/users/model/Session.scala
@@ -1,17 +1,87 @@
package io.branchtalk.users.model
-import io.branchtalk.shared.model.{ FastEq, ID, ShowPretty }
-import io.scalaland.catnip.Semi
+import java.time.{ Instant, OffsetDateTime, ZoneId }
+import java.time.format.DateTimeFormatter
-@Semi(FastEq, ShowPretty) final case class Session(
+import cats.{ Functor, Order, Show }
+import cats.effect.Clock
+import enumeratum.{ Enum, EnumEntry }
+import enumeratum.EnumEntry.Hyphencase
+import io.branchtalk.shared.model.*
+
+final case class Session(
id: ID[Session],
data: Session.Data
-)
-object Session extends SessionProperties with SessionCommands {
+) derives FastEq,
+ ShowPretty
+object Session {
+
+ final case class Data(
+ userID: ID[User],
+ usage: Session.Usage,
+ expiresAt: Session.ExpirationTime
+ ) derives FastEq,
+ ShowPretty
- @Semi(FastEq, ShowPretty) final case class Data(
+ final case class Create(
userID: ID[User],
usage: Session.Usage,
expiresAt: Session.ExpirationTime
- )
+ ) derives FastEq,
+ ShowPretty
+
+ final case class Delete(
+ id: ID[Session]
+ ) derives FastEq,
+ ShowPretty
+
+ type ExpirationTime = ExpirationTime.Type
+ object ExpirationTime extends Newtype[OffsetDateTime] {
+
+ def unapply(expirationTime: ExpirationTime): Some[OffsetDateTime] = Some(expirationTime.unwrap)
+
+ def now[F[_]: Functor: Clock]: F[ExpirationTime] =
+ Clock[F].realTime
+ .map(_.toMillis)
+ .map(Instant.ofEpochMilli)
+ .map(OffsetDateTime.ofInstant(_, ZoneId.systemDefault()))
+ .pipe(unsafeMakeF)
+
+ given Show[ExpirationTime] = _.unwrap.pipe(DateTimeFormatter.ISO_INSTANT.format)
+ given Order[ExpirationTime] = Order.by[ExpirationTime, OffsetDateTime](_.unwrap)(Order.fromComparable)
+
+ extension (time: ExpirationTime) {
+ def plusDays(days: Long): ExpirationTime = ExpirationTime(time.unwrap.plusDays(days))
+ }
+ }
+
+ enum Usage derives FastEq, ShowPretty {
+ case UserSession
+ case OAuth(permissions: Permissions)
+ }
+ object Usage {
+
+ enum Type extends EnumEntry with Hyphencase derives FastEq, ShowPretty {
+ case UserSession
+ case OAuth
+ }
+
+ object Tupled {
+ def apply(usageType: Type, usagePermissions: Permissions): Usage = usageType match {
+ case Type.UserSession => Usage.UserSession
+ case Type.OAuth => Usage.OAuth(usagePermissions)
+ }
+
+ def unpack(usage: Usage): (Type, Permissions) = usage match {
+ case UserSession => (Type.UserSession, Permissions(Set.empty))
+ case OAuth(permissions) => (Type.OAuth, permissions)
+ }
+
+ def unapply(usage: Usage): Some[(Type, Permissions)] = Some(unpack(usage))
+ }
+ }
+
+ enum Sorting derives FastEq, ShowPretty {
+ case ClosestToExpiry
+ }
}
diff --git a/modules/users/src/main/scala/io/branchtalk/users/model/SessionCommands.scala b/modules/users/src/main/scala/io/branchtalk/users/model/SessionCommands.scala
deleted file mode 100644
index 3ea0421d..00000000
--- a/modules/users/src/main/scala/io/branchtalk/users/model/SessionCommands.scala
+++ /dev/null
@@ -1,23 +0,0 @@
-package io.branchtalk.users.model
-
-import io.branchtalk.shared.model.{ FastEq, ID, ShowPretty }
-import io.scalaland.catnip.Semi
-
-trait SessionCommands {
- type Create = SessionCommands.Create
- type Delete = SessionCommands.Delete
- val Create = SessionCommands.Create
- val Delete = SessionCommands.Delete
-}
-object SessionCommands {
-
- @Semi(FastEq, ShowPretty) final case class Create(
- userID: ID[User],
- usage: Session.Usage,
- expiresAt: Session.ExpirationTime
- )
-
- @Semi(FastEq, ShowPretty) final case class Delete(
- id: ID[Session]
- )
-}
diff --git a/modules/users/src/main/scala/io/branchtalk/users/model/SessionProperties.scala b/modules/users/src/main/scala/io/branchtalk/users/model/SessionProperties.scala
deleted file mode 100644
index f42e5f3f..00000000
--- a/modules/users/src/main/scala/io/branchtalk/users/model/SessionProperties.scala
+++ /dev/null
@@ -1,80 +0,0 @@
-package io.branchtalk.users.model
-
-import java.time.{ Instant, OffsetDateTime, ZoneId }
-import java.time.format.DateTimeFormatter
-
-import cats.{ Functor, Order, Show }
-import cats.effect.Clock
-import enumeratum.{ Enum, EnumEntry }
-import enumeratum.EnumEntry.Hyphencase
-import io.branchtalk.ADT
-import io.branchtalk.shared.model._
-import io.estatico.newtype.macros.newtype
-import io.scalaland.catnip.Semi
-
-trait SessionProperties {
- type ExpirationTime = SessionProperties.ExpirationTime
- type Usage = SessionProperties.Usage
- type Sorting = SessionProperties.Sorting
- val ExpirationTime = SessionProperties.ExpirationTime
- val Usage = SessionProperties.Usage
- val Sorting = SessionProperties.Sorting
-}
-object SessionProperties {
-
- @newtype final case class ExpirationTime(offsetDateTime: OffsetDateTime) {
-
- def plusDays(days: Long): ExpirationTime = ExpirationTime(offsetDateTime.plusDays(days))
- }
-
- object ExpirationTime {
- def unapply(expirationTime: ExpirationTime): Some[OffsetDateTime] = Some(expirationTime.offsetDateTime)
-
- def now[F[_]: Functor: Clock]: F[ExpirationTime] =
- Clock[F].realTime
- .map(_.toMillis)
- .map(Instant.ofEpochMilli)
- .map(OffsetDateTime.ofInstant(_, ZoneId.systemDefault()))
- .map(ExpirationTime(_))
-
- implicit val show: Show[ExpirationTime] = Show.wrap(_.offsetDateTime.pipe(DateTimeFormatter.ISO_INSTANT.format))
- implicit val order: Order[ExpirationTime] =
- Order.by[ExpirationTime, OffsetDateTime](_.offsetDateTime)(Order.fromComparable)
- }
-
- @Semi(FastEq, ShowPretty) sealed trait Usage extends ADT
-
- object Usage {
- case object UserSession extends Usage
- final case class OAuth(permissions: Permissions) extends Usage
-
- @Semi(FastEq, ShowPretty) sealed trait Type extends EnumEntry with Hyphencase
- object Type extends Enum[Type] {
- case object UserSession extends Type
- case object OAuth extends Type
-
- val values: IndexedSeq[Type] = findValues
- }
-
- object Tupled {
- def apply(usageType: Type, usagePermissions: Permissions): Usage = usageType match {
- case Type.UserSession => Usage.UserSession
- case Type.OAuth => Usage.OAuth(usagePermissions)
- }
-
- def unpack(usage: Usage): (Type, Permissions) = usage match {
- case UserSession => (Type.UserSession, Permissions(Set.empty))
- case OAuth(permissions) => (Type.OAuth, permissions)
- }
-
- def unapply(usage: Usage): Some[(Type, Permissions)] = Some(unpack(usage))
- }
- }
-
- sealed trait Sorting extends EnumEntry
- object Sorting extends Enum[Sorting] {
- case object ClosestToExpiry extends Sorting
-
- val values = findValues
- }
-}
diff --git a/modules/users/src/main/scala/io/branchtalk/users/model/User.scala b/modules/users/src/main/scala/io/branchtalk/users/model/User.scala
index 0a97274c..f9ffe058 100644
--- a/modules/users/src/main/scala/io/branchtalk/users/model/User.scala
+++ b/modules/users/src/main/scala/io/branchtalk/users/model/User.scala
@@ -1,15 +1,17 @@
package io.branchtalk.users.model
-import io.branchtalk.shared.model.{ CreationTime, FastEq, ID, ModificationTime, ShowPretty }
-import io.scalaland.catnip.Semi
+import cats.{ Order, Show }
+import cats.effect.Sync
+import io.branchtalk.shared.model.*
-@Semi(FastEq, ShowPretty) final case class User(
+final case class User(
id: ID[User],
data: User.Data
-)
-object User extends UserProperties with UserCommands {
+) derives FastEq,
+ ShowPretty
+object User {
- @Semi(FastEq, ShowPretty) final case class Data(
+ final case class Data(
email: User.Email, // validate email
username: User.Name,
description: Option[User.Description],
@@ -17,5 +19,82 @@ object User extends UserProperties with UserCommands {
permissions: Permissions,
createdAt: CreationTime,
lastModifiedAt: Option[ModificationTime]
- )
+ ) derives FastEq,
+ ShowPretty
+
+ final case class Create(
+ email: User.Email,
+ username: User.Name,
+ description: Option[User.Description],
+ password: Password
+ ) derives FastEq,
+ ShowPretty
+
+ final case class Update(
+ id: ID[User],
+ moderatorID: Option[ID[User]],
+ newUsername: Updatable[User.Name],
+ newDescription: OptionUpdatable[User.Description],
+ newPassword: Updatable[Password],
+ updatePermissions: List[Permission.Update]
+ ) derives FastEq,
+ ShowPretty
+
+ final case class Delete(
+ id: ID[User],
+ moderatorID: Option[ID[User]]
+ ) derives FastEq,
+ ShowPretty
+
+ final case class Restore(
+ id: ID[User],
+ moderatorID: Option[ID[User]]
+ ) derives FastEq,
+ ShowPretty
+
+ type Email = Email.Type
+ object Email extends Newtype[String] {
+
+ private val pattern = "(.+)@(.+)".r
+
+ override inline def validate(input: String): Boolean = pattern.matches(input)
+
+ def unapply(email: Email): Some[String] = Some(email.unwrap)
+
+ given Show[Email] = unsafeMakeF[Show](Show[String])
+ given Order[Email] = unsafeMakeF[Order](Order[String])
+ }
+
+ type Name = Name.Type
+ object Name extends Newtype[String] {
+
+ override inline def validate(input: String): Boolean = input.nonEmpty
+
+ def unapply(name: Name): Some[String] = Some(name.unwrap)
+
+ given Show[Name] = unsafeMakeF[Show](Show[String])
+ given Order[Name] = unsafeMakeF[Order](Order[String])
+ }
+
+ type Description = Description.Type
+ object Description extends Newtype[String] {
+
+ override inline def validate(input: String): Boolean = input.nonEmpty
+
+ def unapply(description: Description): Some[String] = Some(description.unwrap)
+
+ given Show[Description] = unsafeMakeF[Show](Show[String])
+ given Order[Description] = unsafeMakeF[Order](Order[String])
+ }
+
+ enum Filter {
+ case HasPermission(permission: Permission)
+ case HasPermissions(permissions: Permissions)
+ }
+
+ enum Sorting derives FastEq, ShowPretty {
+ case Newest
+ case NameAlphabetically
+ case EmailAlphabetically
+ }
}
diff --git a/modules/users/src/main/scala/io/branchtalk/users/model/UserCommands.scala b/modules/users/src/main/scala/io/branchtalk/users/model/UserCommands.scala
deleted file mode 100644
index 7da82b8f..00000000
--- a/modules/users/src/main/scala/io/branchtalk/users/model/UserCommands.scala
+++ /dev/null
@@ -1,43 +0,0 @@
-package io.branchtalk.users.model
-
-import io.branchtalk.shared.model.{ FastEq, ID, OptionUpdatable, ShowPretty, Updatable }
-import io.scalaland.catnip.Semi
-
-trait UserCommands {
- type Create = UserCommands.Create
- type Update = UserCommands.Update
- type Delete = UserCommands.Delete
- type Restore = UserCommands.Restore
- val Create = UserCommands.Create
- val Update = UserCommands.Update
- val Delete = UserCommands.Delete
- val Restore = UserCommands.Restore
-}
-object UserCommands {
-
- @Semi(FastEq, ShowPretty) final case class Create(
- email: User.Email,
- username: User.Name,
- description: Option[User.Description],
- password: Password
- )
-
- @Semi(FastEq, ShowPretty) final case class Update(
- id: ID[User],
- moderatorID: Option[ID[User]],
- newUsername: Updatable[User.Name],
- newDescription: OptionUpdatable[User.Description],
- newPassword: Updatable[Password],
- updatePermissions: List[Permission.Update]
- )
-
- @Semi(FastEq, ShowPretty) final case class Delete(
- id: ID[User],
- moderatorID: Option[ID[User]]
- )
-
- @Semi(FastEq, ShowPretty) final case class Restore(
- id: ID[User],
- moderatorID: Option[ID[User]]
- )
-}
diff --git a/modules/users/src/main/scala/io/branchtalk/users/model/UserProperties.scala b/modules/users/src/main/scala/io/branchtalk/users/model/UserProperties.scala
deleted file mode 100644
index 3d2d1a89..00000000
--- a/modules/users/src/main/scala/io/branchtalk/users/model/UserProperties.scala
+++ /dev/null
@@ -1,73 +0,0 @@
-package io.branchtalk.users.model
-
-import cats.{ Order, Show }
-import cats.effect.Sync
-import enumeratum.{ Enum, EnumEntry }
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.collection.NonEmpty
-import eu.timepit.refined.string.MatchesRegex
-import eu.timepit.refined.types.string.NonEmptyString
-import io.branchtalk.ADT
-import io.branchtalk.shared.model._
-import io.estatico.newtype.macros.newtype
-import io.estatico.newtype.ops._
-
-trait UserProperties {
- type Email = UserProperties.Email
- type Name = UserProperties.Name
- type Description = UserProperties.Description
- type Filter = UserProperties.Filter
- type Sorting = UserProperties.Sorting
- val Email = UserProperties.Email
- val Name = UserProperties.Name
- val Description = UserProperties.Description
- val Filter = UserProperties.Filter
- val Sorting = UserProperties.Sorting
-}
-object UserProperties {
-
- @newtype final case class Email(emailString: String Refined MatchesRegex[Email.Pattern])
- object Email {
- type Pattern = "(.+)@(.+)"
-
- def unapply(email: Email): Some[String Refined MatchesRegex[Email.Pattern]] = Some(email.emailString)
- def parse[F[_]: Sync](string: String): F[Email] =
- ParseRefined[F].parse[MatchesRegex[Email.Pattern]](string).map(Email.apply)
-
- implicit val show: Show[Email] = Show.wrap(_.emailString.value)
- implicit val order: Order[Email] = Order.by(_.emailString.value)
- }
-
- @newtype final case class Name(nonEmptyString: NonEmptyString)
- object Name {
- def unapply(name: Name): Some[NonEmptyString] = Some(name.nonEmptyString)
- def parse[F[_]: Sync](string: String): F[Name] =
- ParseRefined[F].parse[NonEmpty](string).map(Name.apply)
-
- implicit val show: Show[Name] = Show.wrap(_.nonEmptyString.value)
- implicit val order: Order[Name] = Order.by(_.nonEmptyString.value)
- }
-
- @newtype final case class Description(string: String)
- object Description {
- def unapply(description: Description): Some[String] = Some(description.string)
-
- implicit val show: Show[Description] = Show.wrap(_.string)
- implicit val eq: Order[Description] = Order[String].coerce
- }
-
- sealed trait Filter extends ADT
- object Filter {
- final case class HasPermission(permission: Permission) extends Filter
- final case class HasPermissions(permissions: Permissions) extends Filter
- }
-
- sealed trait Sorting extends EnumEntry
- object Sorting extends Enum[Sorting] {
- case object Newest extends Sorting
- case object NameAlphabetically extends Sorting
- case object EmailAlphabetically extends Sorting
-
- val values = findValues
- }
-}
diff --git a/modules/users/src/main/scala/io/branchtalk/users/model/package.scala b/modules/users/src/main/scala/io/branchtalk/users/model/package.scala
deleted file mode 100644
index fab10fcd..00000000
--- a/modules/users/src/main/scala/io/branchtalk/users/model/package.scala
+++ /dev/null
@@ -1,39 +0,0 @@
-package io.branchtalk.users
-
-import cats.{ Eq, Eval, Show }
-import io.branchtalk.shared.model.ShowCompanionOps
-import io.estatico.newtype.macros.newtype
-
-package object model {
-
- @newtype final case class Permissions(set: Set[Permission]) {
-
- def append(permission: Permission): Permissions = Permissions(set + permission)
- def remove(permission: Permission): Permissions = Permissions(set - permission)
-
- def allow(permissions: RequiredPermissions): Boolean = Permissions.validatePermissions(permissions, this)
- def intersect(permissions: Permissions): Permissions = Permissions(set intersect permissions.set)
- }
- object Permissions {
- def unapply(permissions: Permissions): Some[Set[Permission]] = Some(permissions.set)
-
- def empty: Permissions = Permissions(Set.empty)
-
- implicit val show: Show[Permissions] = Show.wrap(_.set.mkString(", "))
- implicit val eq: Eq[Permissions] = Eq.by(_.set)
-
- @SuppressWarnings(Array("org.wartremover.warts.All")) // Eval should be stack-safe
- def validatePermissions(required: RequiredPermissions, existing: Permissions): Boolean = {
- def permitted(permission: Permission) = existing.set.contains(permission)
- def evaluate(req: RequiredPermissions): Eval[Boolean] = req match {
- case RequiredPermissions.Empty => Eval.True
- case RequiredPermissions.AllOf(set) => Eval.later(set.forall(permitted))
- case RequiredPermissions.AnyOf(set) => Eval.later(set.exists(permitted))
- case RequiredPermissions.And(x, y) => (evaluate(x), evaluate(y)).mapN(_ && _)
- case RequiredPermissions.Or(x, y) => (evaluate(x), evaluate(y)).mapN(_ || _)
- case RequiredPermissions.Not(x) => evaluate(x).map(!_)
- }
- evaluate(required).value
- }
- }
-}
diff --git a/modules/users/src/main/scala/io/branchtalk/users/reads/BanReads.scala b/modules/users/src/main/scala/io/branchtalk/users/reads/BanReads.scala
index 271a04f2..2c00ecba 100644
--- a/modules/users/src/main/scala/io/branchtalk/users/reads/BanReads.scala
+++ b/modules/users/src/main/scala/io/branchtalk/users/reads/BanReads.scala
@@ -7,5 +7,5 @@ trait BanReads[F[_]] {
def findForUser(userID: ID[User]): F[Set[Ban]]
def findForChannel(channelID: ID[Channel]): F[Set[Ban]]
- def findGlobally: F[Set[Ban]]
+ def findGlobally: F[Set[Ban]]
}
diff --git a/modules/users/src/main/scala/io/branchtalk/users/reads/SessionReads.scala b/modules/users/src/main/scala/io/branchtalk/users/reads/SessionReads.scala
index 5b8bf5a1..802c1e18 100644
--- a/modules/users/src/main/scala/io/branchtalk/users/reads/SessionReads.scala
+++ b/modules/users/src/main/scala/io/branchtalk/users/reads/SessionReads.scala
@@ -1,7 +1,5 @@
package io.branchtalk.users.reads
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.numeric.{ NonNegative, Positive }
import io.branchtalk.shared.model.{ ID, Paginated }
import io.branchtalk.users.model.{ Session, User }
@@ -10,8 +8,8 @@ trait SessionReads[F[_]] {
def paginate(
user: ID[User],
sortBy: Session.Sorting,
- offset: Long Refined NonNegative,
- limit: Int Refined Positive
+ offset: Paginated.Offset,
+ limit: Paginated.Limit
): F[Paginated[Session]]
def requireById(id: ID[Session]): F[Session]
diff --git a/modules/users/src/main/scala/io/branchtalk/users/reads/UserReads.scala b/modules/users/src/main/scala/io/branchtalk/users/reads/UserReads.scala
index 4441d7af..ea645687 100644
--- a/modules/users/src/main/scala/io/branchtalk/users/reads/UserReads.scala
+++ b/modules/users/src/main/scala/io/branchtalk/users/reads/UserReads.scala
@@ -1,7 +1,5 @@
package io.branchtalk.users.reads
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.numeric.{ NonNegative, Positive }
import io.branchtalk.shared.model.{ ID, Paginated }
import io.branchtalk.users.model.{ Password, User }
@@ -11,8 +9,8 @@ trait UserReads[F[_]] {
def paginate(
sortBy: User.Sorting,
- offset: Long Refined NonNegative,
- limit: Int Refined Positive,
+ offset: Paginated.Offset,
+ limit: Paginated.Limit,
filters: List[User.Filter] = List.empty
): F[Paginated[User]]
diff --git a/project/Dependencies.scala b/project/Dependencies.scala
index ae84a951..f9c72d75 100644
--- a/project/Dependencies.scala
+++ b/project/Dependencies.scala
@@ -1,210 +1,101 @@
import sbt._
-import sbt.Keys.{ libraryDependencies, scalaBinaryVersion }
-import Dependencies._
-import sbtcrossproject.CrossProject
-import sbtcrossproject.CrossPlugin.autoImport._
-import org.portablescala.sbtplatformdeps.PlatformDepsPlugin.autoImport._
-import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport.scalaJSVersion
-
-import Dependencies._
object Dependencies {
// scala version
- val scalaOrganization = "org.scala-lang"
- val scalaVersion = "2.13.9"
- val crossScalaVersions = Seq("2.13.9")
+ val scalaVersion = "3.4.2"
// libraries versions
- val avro4sVersion = "4.1.0" // https://github.com/sksamuel/avro4s/releases
- val catsVersion = "2.8.0" // https://github.com/typelevel/cats/releases
- val catsEffectVersion = "3.3.14" // https://github.com/typelevel/cats-effect/releases
- val declineVersion = "2.3.0" // https://github.com/tpolecat/doobie/releases
- val doobieVersion = "1.0.0-RC2" // https://github.com/tpolecat/doobie/releases
+ val avro4sVersion = "5.0.13" // https://github.com/sksamuel/avro4s/releases
+ val catsVersion = "2.12.0" // https://github.com/typelevel/cats/releases
+ val catsEffectVersion = "3.5.0" // https://github.com/typelevel/cats-effect/releases
+ val declineVersion = "2.4.1" // https://github.com/bkirwi/decline/releases
+ val doobieVersion = "1.0.0-RC5" // https://github.com/tpolecat/doobie/releases
val drosteVersion = "0.9.0" // https://github.com/higherkindness/droste/releases
- val enumeratumVersion = "1.7.0" // https://github.com/lloydmeta/enumeratum/releases
- val fs2Version = "3.2.14" // https://github.com/typelevel/fs2/releases
- val log4catsVersion = "2.5.0" // https://github.com/ChristopherDavenport/log4cats/releases
- val http4sVersion = "0.24.1" // https://github.com/http4s/http4s/releases
- val jsoniterVersion = "2.17.4" // https://github.com/plokhotnyuk/jsoniter-scala/releases
- val monocleVersion = "3.1.0" // https://github.com/optics-dev/Monocle/releases
- val pureConfigVersion = "0.17.1" // https://github.com/pureconfig/pureconfig/releases
- val refinedVersion = "0.10.1" // https://github.com/fthomas/refined/releases
- val specs2Version = "4.16.1" // https://github.com/etorreborre/specs2/releases
- val tapirVersion = "1.1.0" // https://github.com/softwaremill/tapir/releases
-
- // resolvers
- val resolvers = Seq(
- Resolver.sonatypeOssRepos("public"),
- Resolver.sonatypeOssRepos("releases"),
- Seq(Resolver.typesafeRepo("releases"))
- ).flatten
+ val enumeratumVersion = "1.7.4" // https://github.com/lloydmeta/enumeratum/releases
+ val fs2Version = "3.10.2" // https://github.com/typelevel/fs2/releases
+ val log4catsVersion = "2.7.0" // https://github.com/ChristopherDavenport/log4cats/releases
+ val http4sVersion = "0.24.7" // https://github.com/http4s/http4s/releases
+ val jsoniterVersion = "2.30.7" // https://github.com/plokhotnyuk/jsoniter-scala/releases
+ val neotypeVersion = "0.3.0" // https://github.com/kitlangton/neotype/releases
+ val pureConfigVersion = "0.17.7" // https://github.com/pureconfig/pureconfig/releases
+ val specs2Version = "5.5.3" // https://github.com/etorreborre/specs2/releases
+ val tapirVersion = "1.10.14" // https://github.com/softwaremill/tapir/releases
- // compiler plugins
- val betterMonadicFor =
- "com.olegpy" %% "better-monadic-for" % "0.3.1" // https://github.com/oleg-py/better-monadic-for/releases
- val kindProjector =
- "org.typelevel" %% "kind-projector" % "0.13.2" cross CrossVersion.full // https://github.com/typelevel/kind-projector/releases
// functional libraries
- val catnip =
- "io.scalaland" %% "catnip" % "1.1.2" exclude ("org.typelevel", "kittens_2.13") // https://github.com/scalalandio/catnip/releases
- val cats = "org.typelevel" %% "cats-core" % catsVersion
- val catsFree = "org.typelevel" %% "cats-free" % catsVersion
- val catsEffect = "org.typelevel" %% "cats-effect" % catsEffectVersion
- val alleycats = "org.typelevel" %% "alleycats-core" % catsVersion
- val kittens = "org.typelevel" %% "kittens" % "3.0.0" // https://github.com/typelevel/kittens/releases
- val catsLaws = "org.typelevel" %% "cats-laws" % catsVersion
- val chimney = "io.scalaland" %% "chimney" % "0.6.2" // https://github.com/scalalandio/chimney/releases
- val droste = "io.higherkindness" %% "droste-core" % drosteVersion
- val enumeratum = "com.beachape" %% "enumeratum" % enumeratumVersion
- val fastuuid = "com.eatthepath" % "fast-uuid" % "0.2.0" // https://github.com/jchambers/fast-uuid/releases
+ val cats = "org.typelevel" %% "cats-core" % catsVersion
+ val catsFree = "org.typelevel" %% "cats-free" % catsVersion
+ val catsEffect = "org.typelevel" %% "cats-effect" % catsEffectVersion
+ val alleycats = "org.typelevel" %% "alleycats-core" % catsVersion
+ val kittens = "org.typelevel" %% "kittens" % "3.3.0" // https://github.com/typelevel/kittens/releases
+ val catsLaws = "org.typelevel" %% "cats-laws" % catsVersion
+ val chimney = "io.scalaland" %% "chimney" % "1.3.0" // https://github.com/scalalandio/chimney/releases
+ val droste = "io.higherkindness" %% "droste-core" % drosteVersion
+ val enumeratum = "com.beachape" %% "enumeratum" % enumeratumVersion
+ val enumeratumDoobie = "com.beachape" %% "enumeratum-doobie" % "1.7.6"
+ val fastuuid = "com.eatthepath" % "fast-uuid" % "0.2.0" // https://github.com/jchambers/fast-uuid/releases
val uuidGenerator =
- "com.fasterxml.uuid" % "java-uuid-generator" % "4.0.1" // https://github.com/cowtowncoder/java-uuid-generator/releases
+ "com.fasterxml.uuid" % "java-uuid-generator" % "5.1.0" // https://github.com/cowtowncoder/java-uuid-generator/releases
val fs2 = "co.fs2" %% "fs2-core" % fs2Version
val fs2IO = "co.fs2" %% "fs2-io" % fs2Version
val magnolia =
- "com.softwaremill.magnolia1_2" %% "magnolia" % "1.1.2" // https://github.com/softwaremill/magnolia/releases
- val monocle = "dev.optics" %% "monocle-core" % monocleVersion
- val monocleMacro = "dev.optics" %% "monocle-macro" % monocleVersion
- val newtype = "io.estatico" %% "newtype" % "0.4.4" // https://github.com/estatico/scala-newtype/releases
- val refined = "eu.timepit" %% "refined" % refinedVersion
- val refinedCats = "eu.timepit" %% "refined-cats" % refinedVersion
- val refinedDecline = "com.monovore" %% "decline-refined" % declineVersion
- val refinedPureConfig = "eu.timepit" %% "refined-pureconfig" % refinedVersion
+ "com.softwaremill.magnolia1_3" %% "magnolia" % "1.3.7" // https://github.com/softwaremill/magnolia/releases
+ val neotype = "io.github.kitlangton" %% "neotype" % neotypeVersion
+ val neotypeChimney = "io.github.kitlangton" %% "neotype-chimney" % neotypeVersion
+ val neotypeDoobie = "io.github.kitlangton" %% "neotype-doobie" % neotypeVersion
+ val neotypeJsoniter = "io.github.kitlangton" %% "neotype-jsoniter" % neotypeVersion
+ val neotypeTapir = "io.github.kitlangton" %% "neotype-tapir" % neotypeVersion
+ val quicklens =
+ "com.softwaremill.quicklens" %% "quicklens" % "1.9.7" // https://github.com/softwaremill/quicklens/releases
// infrastructure
val avro4s = "com.sksamuel.avro4s" %% "avro4s-core" % avro4sVersion
val avro4sCats = "com.sksamuel.avro4s" %% "avro4s-cats" % avro4sVersion
- val avro4sRefined = "com.sksamuel.avro4s" %% "avro4s-refined" % avro4sVersion
val doobie = "org.tpolecat" %% "doobie-core" % doobieVersion
val doobieHikari = "org.tpolecat" %% "doobie-hikari" % doobieVersion
val doobiePostgres = "org.tpolecat" %% "doobie-postgres" % doobieVersion
- val doobieRefined = "org.tpolecat" %% "doobie-refined" % doobieVersion
val doobieSpecs2 = "org.tpolecat" %% "doobie-specs2" % doobieVersion
- val flyway = "org.flywaydb" % "flyway-core" % "9.3.0" // https://github.com/flyway/flyway/releases
- val fs2Kafka = "com.github.fd4s" %% "fs2-kafka" % "2.5.0" // https://github.com/fd4s/fs2-kafka/releases
- val macwire =
- "com.softwaremill.macwire" %% "macros" % "2.5.8" % "provided" // https://github.com/softwaremill/macwire/releases
+ val flyway = "org.flywaydb" % "flyway-core" % "10.16.0" // https://github.com/flyway/flyway/releases
+ val flywayPostgres = "org.flywaydb" % "flyway-database-postgresql" % "10.16.0" // https://github.com/flyway/flyway/releases
+ val fs2Kafka = "com.github.fd4s" %% "fs2-kafka" % "3.5.1" // https://github.com/fd4s/fs2-kafka/releasesreleases
val redis4cats =
- "dev.profunktor" %% "redis4cats-effects" % "1.2.0" // https://github.com/profunktor/redis4cats/releases
+ "dev.profunktor" %% "redis4cats-effects" % "1.7.1" // https://github.com/profunktor/redis4cats/releases
// API
val sttpCats =
- "com.softwaremill.sttp.client3" %% "async-http-client-backend-cats" % "3.8.0" // https://github.com/softwaremill/sttp/releases
+ "com.softwaremill.sttp.client3" %% "async-http-client-backend-cats" % "3.9.7" // https://github.com/softwaremill/sttp/releases
// same as the one used by tapir
- val http4sBlaze = "org.http4s" %% "http4s-blaze-server" % "0.23.12" // https://github.com/http4s/blaze/releases
+ val http4sBlaze = "org.http4s" %% "http4s-blaze-server" % "0.23.16" // https://github.com/http4s/blaze/releases
val http4sPrometheus = "org.http4s" %% "http4s-prometheus-metrics" % http4sVersion
val tapir = "com.softwaremill.sttp.tapir" %% "tapir-core" % tapirVersion
val tapirHttp4s = "com.softwaremill.sttp.tapir" %% "tapir-http4s-server" % tapirVersion
val tapirJsoniter = "com.softwaremill.sttp.tapir" %% "tapir-jsoniter-scala" % tapirVersion
- val tapirRefined = "com.softwaremill.sttp.tapir" %% "tapir-refined" % tapirVersion
val tapirOpenAPI = "com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % tapirVersion
val tapirSwaggerUI = "com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-http4s" % "0.19.0-M4" // tapirVersion
val tapirSTTP = "com.softwaremill.sttp.tapir" %% "tapir-sttp-client" % tapirVersion
val jsoniter = "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % jsoniterVersion
val jsoniterMacro =
- "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % jsoniterVersion % "compile-internal"
+ "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % jsoniterVersion // % "compile-internal"
// config
val decline = "com.monovore" %% "decline" % declineVersion
- val scalaConfig = "com.typesafe" % "config" % "1.4.2" // https://github.com/lightbend/config/releases
- val pureConfig = "com.github.pureconfig" %% "pureconfig" % pureConfigVersion
+ val scalaConfig = "com.typesafe" % "config" % "1.4.3" // https://github.com/lightbend/config/releases
+ val pureConfig = "com.github.pureconfig" %% "pureconfig-core" % pureConfigVersion
val pureConfigCats = "com.github.pureconfig" %% "pureconfig-cats" % pureConfigVersion
val pureConfigEnumeratum = "com.github.pureconfig" %% "pureconfig-enumeratum" % pureConfigVersion
// security
- val bcrypt = "at.favre.lib" % "bcrypt" % "0.9.0"
+ val bcrypt = "at.favre.lib" % "bcrypt" % "0.10.2"
// logging
val log4cats = "org.typelevel" %% "log4cats-core" % log4catsVersion
val log4catsSlf4j = "org.typelevel" %% "log4cats-slf4j" % log4catsVersion
val scalaLogging = "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5" // GH releases are out of date
- val logback = "ch.qos.logback" % "logback-classic" % "1.4.1" // https://github.com/qos-ch/logback/releases
+ val logback = "ch.qos.logback" % "logback-classic" % "1.5.6" // https://github.com/qos-ch/logback/releases
val logbackJackson = "ch.qos.logback.contrib" % "logback-jackson" % "0.1.5" // see MVN
val logbackJsonClassic = "ch.qos.logback.contrib" % "logback-json-classic" % "0.1.5" // see MVN
- val sourcecode = "com.lihaoyi" %% "sourcecode" % "0.3.0" // https://github.com/lihaoyi/sourcecode/releases
- val prometheus = "io.prometheus" % "simpleclient" % "0.16.0" // https://github.com/prometheus/client_java/releases
+ val sourcecode = "com.lihaoyi" %% "sourcecode" % "0.4.2" // https://github.com/lihaoyi/sourcecode/releases
+ val prometheus = "io.prometheus" % "simpleclient" % "0.16.0" // https://github.com/prometheus/client_java/releases
// testing
- val jfairy = "com.devskiller" % "jfairy" % "0.6.4" // https://github.com/Devskiller/jfairy/releases
- val guice = "com.google.inject" % "guice" % "5.0.1" // required by jfairy on JDK 15+
+ val jfairy = "com.devskiller" % "jfairy" % "0.6.5" // https://github.com/Devskiller/jfairy/releases
+ val guice = "com.google.inject" % "guice" % "7.0.0" // required by jfairy on JDK 15+
val guiceAssisted =
- "com.google.inject.extensions" % "guice-assistedinject" % "5.0.1" // required by jfairy on JDK 15+
+ "com.google.inject.extensions" % "guice-assistedinject" % "7.0.0" // required by jfairy on JDK 15+
val spec2Core = "org.specs2" %% "specs2-core" % specs2Version
val spec2Scalacheck = "org.specs2" %% "specs2-scalacheck" % specs2Version
}
-
-trait Dependencies {
-
- val scalaOrganizationUsed = scalaOrganization
- val scalaVersionUsed = scalaVersion
- val crossScalaVersionsUsed = crossScalaVersions
-
- // resolvers
- val commonResolvers = resolvers
-
- val mainDeps = Seq(
- cats,
- catsFree,
- catsEffect,
- alleycats,
- kittens,
- chimney,
- enumeratum,
- fastuuid,
- uuidGenerator,
- log4cats,
- log4catsSlf4j,
- magnolia,
- monocle,
- monocleMacro,
- newtype,
- refined,
- refinedCats,
- scalaLogging,
- logback
- )
-
- val testDeps = Seq(catsLaws, spec2Core, spec2Scalacheck)
-
- implicit final class ProjectRoot(project: Project) {
-
- def root: Project = project in file(".")
- }
-
- implicit final class ProjectFrom(project: Project) {
-
- private val commonDir = "modules"
-
- def from(dir: String): Project = project in file(s"$commonDir/$dir")
- }
-
- implicit final class DependsOnProject(project: Project) {
-
- private val testConfigurations = Set("test", "fun", "it")
- private def findCompileAndTestConfigs(p: Project) =
- (p.configurations.map(_.name).toSet intersect testConfigurations) + "compile"
-
- private val thisProjectsConfigs = findCompileAndTestConfigs(project)
- private def generateDepsForProject(p: Project) =
- p % (thisProjectsConfigs intersect findCompileAndTestConfigs(p) map (c => s"$c->$c") mkString ";")
-
- def compileAndTestDependsOn(projects: Project*): Project =
- project dependsOn (projects.map(generateDepsForProject): _*)
- }
-
- implicit final class CrossProjectFrom(project: CrossProject) {
-
- private val commonDir = "modules"
-
- def from(dir: String): CrossProject = project in file(s"$commonDir/$dir")
- }
-
- implicit final class DependsOnCrossProject(project: CrossProject) {
-
- private val testConfigurations = Set("test", "fun", "it")
- private def findCompileAndTestConfigs(p: CrossProject) =
- (p.projects(JVMPlatform).configurations.map(_.name).toSet intersect testConfigurations) + "compile"
-
- private val thisProjectsConfigs = findCompileAndTestConfigs(project)
- private def generateDepsForProject(p: CrossProject) =
- p % (thisProjectsConfigs intersect findCompileAndTestConfigs(p) map (c => s"$c->$c") mkString ";")
-
- def compileAndTestDependsOn(projects: CrossProject*): CrossProject =
- project dependsOn (projects.map(generateDepsForProject): _*)
- }
-}
diff --git a/project/Settings.scala b/project/Settings.scala
deleted file mode 100644
index 4631eb0b..00000000
--- a/project/Settings.scala
+++ /dev/null
@@ -1,292 +0,0 @@
-import sbt._
-import sbt.Keys._
-import sbt.TestFrameworks.Specs2
-import sbt.Tests.Argument
-import com.github.sbt.git.GitVersioning
-import com.typesafe.sbt._
-import com.typesafe.sbt.packager.archetypes.JavaAppPackaging
-import com.typesafe.sbt.packager.docker.DockerPlugin
-import org.scalafmt.sbt.ScalafmtPlugin.scalafmtConfigSettings
-import org.scalafmt.sbt.ScalafmtPlugin.autoImport._
-import org.scalastyle.sbt.ScalastylePlugin.autoImport._
-import sbtassembly.AssemblyPlugin.autoImport._
-import sbtcrossproject.CrossPlugin.autoImport.JVMPlatform
-import sbtcrossproject.CrossProject
-import scoverage._
-import wartremover.WartRemover.autoImport._
-
-object Settings extends Dependencies {
-
- val FunctionalTest: Configuration = config("fun") extend Test describedAs "Runs only functional tests"
-
- private val commonSettings = Seq(
- organization := "io.branchtalk",
- scalaOrganization := scalaOrganizationUsed,
- scalaVersion := scalaVersionUsed,
- crossScalaVersions := crossScalaVersionsUsed,
- // kind of required to avoid "{project}/doc" task failure, because of Catnip :/
- Compile / doc / sources := Seq.empty,
- Compile / packageDoc / publishArtifact := false
- )
-
- private val rootSettings = commonSettings
-
- private val modulesSettings = commonSettings ++ Seq(
- scalacOptions ++= Seq(
- // standard settings
- "-encoding",
- "UTF-8",
- "-unchecked",
- "-deprecation",
- "-explaintypes",
- "-feature",
- // language features
- "-language:existentials",
- "-language:higherKinds",
- "-language:implicitConversions",
- "-language:postfixOps",
- // private options
- "-Ybackend-parallelism",
- "8",
- "-Ymacro-annotations",
- "-Wmacros:before",
- // warnings
- "-Ywarn-dead-code",
- "-Ywarn-extra-implicit",
- "-Ywarn-macros:before",
- "-Ywarn-numeric-widen",
- //"-Ywarn-unused", // TODO: a lot of new false-positive errors after bumping from 2.13.4 to 2.13.5
- "-Ywarn-unused:implicits",
- "-Ywarn-unused:imports",
- "-Ywarn-unused:imports",
- "-Ywarn-unused:locals",
- "-Ywarn-unused:patvars",
- "-Ywarn-unused:privates",
- "-Ywarn-value-discard",
- // advanced options
- "-Xcheckinit",
- "-Xfatal-warnings",
- // linting
- "-Xlint:adapted-args",
- "-Xlint:constant",
- "-Xlint:delayedinit-select",
- "-Xlint:doc-detached",
- "-Xlint:implicit-recursion",
- "-Xlint:inaccessible",
- "-Xlint:infer-any",
- "-Xlint:missing-interpolator",
- "-Xlint:nullary-unit",
- "-Xlint:option-implicit",
- "-Xlint:package-object-classes",
- "-Xlint:poly-implicit-overload",
- "-Xlint:private-shadow",
- "-Xlint:stars-align",
- "-Xlint:type-parameter-shadow"
- ),
- console / scalacOptions --= Seq(
- // warnings
- "-Ywarn-unused",
- "-Ywarn-unused:implicits",
- "-Ywarn-unused:imports",
- "-Ywarn-unused:imports",
- "-Ywarn-unused:locals",
- "-Ywarn-unused:patvars",
- "-Ywarn-unused:privates",
- // advanced options
- "-Xfatal-warnings",
- // linting
- "-Xlint"
- ),
- Global / cancelable := true,
- Compile / trapExit := false,
- Compile / connectInput := true,
- Compile / outputStrategy := Some(StdoutOutput),
- resolvers ++= commonResolvers,
- libraryDependencies ++= mainDeps,
- addCompilerPlugin(Dependencies.betterMonadicFor),
- addCompilerPlugin(Dependencies.kindProjector),
- Compile / scalafmtOnCompile := true,
- Compile / compile / wartremoverWarnings ++= Warts.allBut(
- Wart.Any,
- Wart.DefaultArguments,
- Wart.ExplicitImplicitTypes,
- Wart.ImplicitConversion,
- Wart.ImplicitParameter,
- Wart.Overloading,
- Wart.PublicInference,
- Wart.NonUnitStatements,
- Wart.Nothing
- ),
- scalastyleFailOnError := true
- )
-
- def customPredef(imports: String*): Def.Setting[Task[Seq[String]]] =
- scalacOptions += s"-Yimports:${(Seq("java.lang", "scala", "scala.Predef") ++ imports).mkString(",")}"
-
- implicit final class RunConfigurator(project: Project) {
-
- def configureRun(main: String): Project =
- project
- .enablePlugins(JavaAppPackaging, DockerPlugin)
- .settings(
- inTask(assembly)(
- Seq(
- assemblyJarName := s"${name.value}.jar",
- assemblyMergeStrategy := {
- // required for OpenAPIServer to work
- case PathList("META-INF", "maven", "org.webjars", "swagger-ui", "pom.properties") =>
- MergeStrategy.singleOrError
- // conflicts on random crap
- case "module-info.class" => MergeStrategy.discard
- // our own Catnip customizations
- case "derive.semi.conf" => MergeStrategy.concat
- case "derive.stub.conf" => MergeStrategy.concat
- // otherwise
- case strategy => MergeStrategy.defaultMergeStrategy(strategy)
- },
- mainClass := Some(main)
- )
- )
- )
- .settings(
- Compile / run / mainClass := Some(main),
- Compile / run / fork := true,
- Compile / runMain / fork := true
- )
- }
-
- sealed abstract class TestConfigurator(project: Project, config: Configuration) {
-
- protected def configure(requiresFork: Boolean): Project =
- project
- .configs(config)
- .settings(inConfig(config)(Defaults.testSettings): _*)
- .settings(inConfig(config)(scalafmtConfigSettings))
- .settings(
- inConfig(config)(
- Seq(
- scalafmtOnCompile := true,
- scalastyleConfig := baseDirectory.value / "scalastyle-test-config.xml",
- scalastyleFailOnError := false,
- fork := requiresFork,
- testFrameworks := Seq(Specs2)
- )
- )
- )
- .settings(libraryDependencies ++= testDeps map (_ % config.name))
- .enablePlugins(ScoverageSbtPlugin)
-
- protected def configureSequential(requiresFork: Boolean): Project =
- configure(requiresFork).settings(
- inConfig(config)(
- Seq(
- testOptions += Argument(Specs2, "sequential"),
- parallelExecution := false
- )
- )
- )
- }
-
- implicit final class DataConfigurator(project: Project) {
-
- def setName(newName: String): Project = project.settings(name := newName)
-
- def setDescription(newDescription: String): Project = project.settings(description := newDescription)
-
- def setInitialImport(newInitialCommand: String*): Project =
- project.settings(initialCommands := s"import ${("io.branchtalk._" +: newInitialCommand).mkString(", ")}")
- }
-
- implicit final class CrossDataConfigurator(project: CrossProject) {
-
- def setName(newName: String): CrossProject = project.configure(_.setName(newName))
-
- def setDescription(newDescription: String): CrossProject = project.configure(_.setDescription(newDescription))
-
- def setInitialImport(newInitialCommand: String*): CrossProject =
- project.configure(_.setInitialImport(newInitialCommand: _*))
- }
-
- implicit final class RootConfigurator(project: Project) {
-
- def configureRoot: Project = project.settings(rootSettings: _*)
- }
-
- implicit final class ModuleConfigurator(project: Project) {
-
- def configureModule: Project = project.settings(modulesSettings: _*).enablePlugins(GitVersioning)
- }
-
- implicit final class UnitTestConfigurator(project: Project) extends TestConfigurator(project, Test) {
-
- def configureTests(requiresFork: Boolean = false): Project = configure(requiresFork)
-
- def configureTestsSequential(requiresFork: Boolean = false): Project = configureSequential(requiresFork)
- }
-
- implicit final class FunctionalTestConfigurator(project: Project) extends TestConfigurator(project, FunctionalTest) {
-
- def configureFunctionalTests(requiresFork: Boolean = false): Project = configure(requiresFork)
-
- def configureFunctionalTestsSequential(requiresFork: Boolean = false): Project = configureSequential(requiresFork)
- }
-
- implicit final class IntegrationTestConfigurator(project: Project)
- extends TestConfigurator(project, IntegrationTest) {
-
- def configureIntegrationTests(requiresFork: Boolean = false): Project = configure(requiresFork)
-
- def configureIntegrationTestsSequential(requiresFork: Boolean = false): Project = configureSequential(requiresFork)
- }
-
- implicit final class CrossModuleConfigurator(project: CrossProject) {
-
- private val testConfigurations = Set("test", "fun", "it")
- private def findCompileAndTestConfigs(p: CrossProject) = {
- val names = p.projects(JVMPlatform).configurations.map(_.name).toSet intersect testConfigurations
- (p.projects(JVMPlatform).configurations.filter(cfg => names(cfg.name)).toSet) + Compile
- }
-
- def configureModule: CrossProject = project
- .configure(_.configureModule)
- .settings(
- // workaround for https://github.com/portable-scala/sbt-crossproject/issues/74
- findCompileAndTestConfigs(project).toList.flatMap(
- inConfig(_) {
- unmanagedResourceDirectories ++= {
- unmanagedSourceDirectories.value
- .map(src => (src / ".." / "resources").getCanonicalFile)
- .filterNot(unmanagedResourceDirectories.value.contains)
- .distinct
- }
- }
- )
- )
- }
-
- implicit final class CrossUnitTestConfigurator(project: CrossProject) {
-
- def configureTests(requiresFork: Boolean = false): CrossProject = project.configure(_.configureTests(requiresFork))
-
- def configureTestsSequential(requiresFork: Boolean = false): CrossProject =
- project.configure(_.configureTestsSequential(requiresFork))
- }
-
- implicit final class CrossFunctionalTestConfigurator(project: CrossProject) {
-
- def configureFunctionalTests(requiresFork: Boolean = false): CrossProject =
- project.configure(_.configureFunctionalTests(requiresFork))
-
- def configureFunctionalTestsSequential(requiresFork: Boolean = false): CrossProject =
- project.configure(_.configureFunctionalTestsSequential(requiresFork))
- }
-
- implicit final class CrossIntegrationTestConfigurator(project: CrossProject) {
-
- def configureIntegrationTests(requiresFork: Boolean = false): CrossProject =
- project.configure(_.configureIntegrationTests(requiresFork))
-
- def configureIntegrationTestsSequential(requiresFork: Boolean = false): CrossProject =
- project.configure(_.configureIntegrationTestsSequential(requiresFork))
- }
-}
diff --git a/project/build.properties b/project/build.properties
index 22af2628..081fdbbc 100644
--- a/project/build.properties
+++ b/project/build.properties
@@ -1 +1 @@
-sbt.version=1.7.1
+sbt.version=1.10.0
diff --git a/project/plugins.sbt b/project/plugins.sbt
index 51ba9904..81e569a6 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -1,17 +1,23 @@
-// Scala.js and cross-compilation
-addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.1.0")
-addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.8.0")
+// git
+addSbtPlugin("com.github.sbt" % "sbt-git" % "2.0.1")
+// linters
+addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2")
+addSbtPlugin("org.wartremover" % "sbt-wartremover" % "3.1.7")
+addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.1.0")
+// cross-compile
+addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.10.0")
+addSbtPlugin("com.indoorvivants" % "sbt-commandmatrix" % "0.0.5")
+addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0")
// publishing
-addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.1.0")
+addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.1")
addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.11")
-addSbtPlugin("com.github.sbt" % "sbt-git" % "2.0.0")
-// linting
-addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6")
-addSbtPlugin("com.beautiful-scala" % "sbt-scalastyle" % "1.5.1")
-addSbtPlugin("org.wartremover" % "sbt-wartremover" % "3.0.6")
-addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.3")
+addSbtPlugin("com.github.sbt" % "sbt-git" % "2.0.1")
+// disabling projects in IDE
+addSbtPlugin("org.jetbrains" % "sbt-ide-settings" % "1.1.0")
// running
addSbtPlugin("io.spray" % "sbt-revolver" % "0.9.1")
libraryDependencies += "org.slf4j" % "slf4j-nop" % "1.7.25"
dependencyOverrides += "org.scala-lang.modules" %% "scala-xml" % "2.1.0"
+
+ThisBuild / libraryDependencySchemes += "org.scala-lang.modules" %% "scala-xml" % VersionScheme.Always
diff --git a/scalastyle-config.xml b/scalastyle-config.xml
deleted file mode 100644
index 2863ab95..00000000
--- a/scalastyle-config.xml
+++ /dev/null
@@ -1,99 +0,0 @@
-
- branchtalk style
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/scalastyle-test-config.xml b/scalastyle-test-config.xml
deleted file mode 100644
index 4ac1ab15..00000000
--- a/scalastyle-test-config.xml
+++ /dev/null
@@ -1,99 +0,0 @@
-
- branchtalk test style
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-