diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala new file mode 100644 index 0000000..65c7e92 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* + +/** Represents aggregate root in DDD + * + * @tparam Id + * Aggregate root id + * @tparam Command + * Aggregate root command + * @tparam Event + * Aggregate root event + * @tparam State + * Aggregate root state + */ +trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: + /** Aggregate root id */ + def id: Id + + /** Current state */ + def state: UIO[State] + + def handle(command: Command): IO[Error, Unit] diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala new file mode 100644 index 0000000..65c7e92 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* + +/** Represents aggregate root in DDD + * + * @tparam Id + * Aggregate root id + * @tparam Command + * Aggregate root command + * @tparam Event + * Aggregate root event + * @tparam State + * Aggregate root state + */ +trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: + /** Aggregate root id */ + def id: Id + + /** Current state */ + def state: UIO[State] + + def handle(command: Command): IO[Error, Unit] diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala new file mode 100644 index 0000000..c051650 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala @@ -0,0 +1,58 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator + +sealed trait FactoryError: + def entityId: String + def userMessage: UserMessage + +object FactoryError: + case class EntityAlreadyExists[Id](entityId: String, id: Id) + extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) + + case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) + +trait AggregateRootFactory[ + Ident, + T <: AggregateRoot[Ident, ?, ?, ?, ?] +]: + type Id = Ident + type NotFound = FactoryError.EntityNotFound[Id] + type AlreadyExists = FactoryError.EntityAlreadyExists[Id] + + protected def AlreadyExists(id: Id): AlreadyExists = + FactoryError.EntityAlreadyExists(entityId, id) + + protected def NotFound(id: Id): NotFound = + FactoryError.EntityNotFound(entityId, id) + + def entityId: String + def make(id: Id): IO[AlreadyExists, T] + def load(id: Id): IO[NotFound, T] + + def loadOrMake(id: Id): UIO[T] = + load(id) + .orElse(make(id)) + .orDieWith(_ => + new RuntimeException( + s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" + ) + ) + + def make()(using idGen: IdGenerator[Id]): UIO[T] = + val tryToCreate = for + id <- idGen.nextId + entity <- make(id) + yield entity + + tryToCreate + .retryN(10) + .orDieWith(_ => + new RuntimeException( + "Cannot create entity, generator failed 10 times to create new ID" + ) + ) diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala new file mode 100644 index 0000000..65c7e92 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* + +/** Represents aggregate root in DDD + * + * @tparam Id + * Aggregate root id + * @tparam Command + * Aggregate root command + * @tparam Event + * Aggregate root event + * @tparam State + * Aggregate root state + */ +trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: + /** Aggregate root id */ + def id: Id + + /** Current state */ + def state: UIO[State] + + def handle(command: Command): IO[Error, Unit] diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala new file mode 100644 index 0000000..c051650 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala @@ -0,0 +1,58 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator + +sealed trait FactoryError: + def entityId: String + def userMessage: UserMessage + +object FactoryError: + case class EntityAlreadyExists[Id](entityId: String, id: Id) + extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) + + case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) + +trait AggregateRootFactory[ + Ident, + T <: AggregateRoot[Ident, ?, ?, ?, ?] +]: + type Id = Ident + type NotFound = FactoryError.EntityNotFound[Id] + type AlreadyExists = FactoryError.EntityAlreadyExists[Id] + + protected def AlreadyExists(id: Id): AlreadyExists = + FactoryError.EntityAlreadyExists(entityId, id) + + protected def NotFound(id: Id): NotFound = + FactoryError.EntityNotFound(entityId, id) + + def entityId: String + def make(id: Id): IO[AlreadyExists, T] + def load(id: Id): IO[NotFound, T] + + def loadOrMake(id: Id): UIO[T] = + load(id) + .orElse(make(id)) + .orDieWith(_ => + new RuntimeException( + s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" + ) + ) + + def make()(using idGen: IdGenerator[Id]): UIO[T] = + val tryToCreate = for + id <- idGen.nextId + entity <- make(id) + yield entity + + tryToCreate + .retryN(10) + .orDieWith(_ => + new RuntimeException( + "Cannot create entity, generator failed 10 times to create new ID" + ) + ) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala new file mode 100644 index 0000000..b2fef3a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala @@ -0,0 +1,24 @@ +package works.iterative.entity + +import works.iterative.core.Validated +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator +import zio.* + +/** Generic class for entity factories. + * + * Creates a new entity from a "seed". + */ +trait EntityFactory[Entity, Seed]: + def make(seed: Seed): IO[UserMessage, Entity] + +/** A factory using idGenerator to create the entity */ +class GeneratingFactory[Entity, Seed, Id]( + constructor: Seed => Validated[Id => Entity], + idGenerator: IdGenerator[Id] +) extends EntityFactory[Entity, Seed]: + override def make(seed: Seed): IO[UserMessage, Entity] = + for + construct <- constructor(seed).toZIO + id <- idGenerator.nextId + yield construct(id) diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala new file mode 100644 index 0000000..65c7e92 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* + +/** Represents aggregate root in DDD + * + * @tparam Id + * Aggregate root id + * @tparam Command + * Aggregate root command + * @tparam Event + * Aggregate root event + * @tparam State + * Aggregate root state + */ +trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: + /** Aggregate root id */ + def id: Id + + /** Current state */ + def state: UIO[State] + + def handle(command: Command): IO[Error, Unit] diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala new file mode 100644 index 0000000..c051650 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala @@ -0,0 +1,58 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator + +sealed trait FactoryError: + def entityId: String + def userMessage: UserMessage + +object FactoryError: + case class EntityAlreadyExists[Id](entityId: String, id: Id) + extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) + + case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) + +trait AggregateRootFactory[ + Ident, + T <: AggregateRoot[Ident, ?, ?, ?, ?] +]: + type Id = Ident + type NotFound = FactoryError.EntityNotFound[Id] + type AlreadyExists = FactoryError.EntityAlreadyExists[Id] + + protected def AlreadyExists(id: Id): AlreadyExists = + FactoryError.EntityAlreadyExists(entityId, id) + + protected def NotFound(id: Id): NotFound = + FactoryError.EntityNotFound(entityId, id) + + def entityId: String + def make(id: Id): IO[AlreadyExists, T] + def load(id: Id): IO[NotFound, T] + + def loadOrMake(id: Id): UIO[T] = + load(id) + .orElse(make(id)) + .orDieWith(_ => + new RuntimeException( + s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" + ) + ) + + def make()(using idGen: IdGenerator[Id]): UIO[T] = + val tryToCreate = for + id <- idGen.nextId + entity <- make(id) + yield entity + + tryToCreate + .retryN(10) + .orDieWith(_ => + new RuntimeException( + "Cannot create entity, generator failed 10 times to create new ID" + ) + ) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala new file mode 100644 index 0000000..b2fef3a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala @@ -0,0 +1,24 @@ +package works.iterative.entity + +import works.iterative.core.Validated +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator +import zio.* + +/** Generic class for entity factories. + * + * Creates a new entity from a "seed". + */ +trait EntityFactory[Entity, Seed]: + def make(seed: Seed): IO[UserMessage, Entity] + +/** A factory using idGenerator to create the entity */ +class GeneratingFactory[Entity, Seed, Id]( + constructor: Seed => Validated[Id => Entity], + idGenerator: IdGenerator[Id] +) extends EntityFactory[Entity, Seed]: + override def make(seed: Seed): IO[UserMessage, Entity] = + for + construct <- constructor(seed).toZIO + id <- idGenerator.nextId + yield construct(id) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala new file mode 100644 index 0000000..29ae384 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.auth.CurrentUser + +trait EntityUpdateService[Id, Command, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def update(id: Id, command: Command): Op[Unit] + +trait EntityCreateService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(initData: Init): Op[Id] + +trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(id: Id, initData: Init): Op[Id] + +trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] + extends EntityCreateService[Id, Init, Error] + with EntityUpdateService[Id, Command, Error] diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala new file mode 100644 index 0000000..65c7e92 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* + +/** Represents aggregate root in DDD + * + * @tparam Id + * Aggregate root id + * @tparam Command + * Aggregate root command + * @tparam Event + * Aggregate root event + * @tparam State + * Aggregate root state + */ +trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: + /** Aggregate root id */ + def id: Id + + /** Current state */ + def state: UIO[State] + + def handle(command: Command): IO[Error, Unit] diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala new file mode 100644 index 0000000..c051650 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala @@ -0,0 +1,58 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator + +sealed trait FactoryError: + def entityId: String + def userMessage: UserMessage + +object FactoryError: + case class EntityAlreadyExists[Id](entityId: String, id: Id) + extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) + + case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) + +trait AggregateRootFactory[ + Ident, + T <: AggregateRoot[Ident, ?, ?, ?, ?] +]: + type Id = Ident + type NotFound = FactoryError.EntityNotFound[Id] + type AlreadyExists = FactoryError.EntityAlreadyExists[Id] + + protected def AlreadyExists(id: Id): AlreadyExists = + FactoryError.EntityAlreadyExists(entityId, id) + + protected def NotFound(id: Id): NotFound = + FactoryError.EntityNotFound(entityId, id) + + def entityId: String + def make(id: Id): IO[AlreadyExists, T] + def load(id: Id): IO[NotFound, T] + + def loadOrMake(id: Id): UIO[T] = + load(id) + .orElse(make(id)) + .orDieWith(_ => + new RuntimeException( + s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" + ) + ) + + def make()(using idGen: IdGenerator[Id]): UIO[T] = + val tryToCreate = for + id <- idGen.nextId + entity <- make(id) + yield entity + + tryToCreate + .retryN(10) + .orDieWith(_ => + new RuntimeException( + "Cannot create entity, generator failed 10 times to create new ID" + ) + ) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala new file mode 100644 index 0000000..b2fef3a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala @@ -0,0 +1,24 @@ +package works.iterative.entity + +import works.iterative.core.Validated +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator +import zio.* + +/** Generic class for entity factories. + * + * Creates a new entity from a "seed". + */ +trait EntityFactory[Entity, Seed]: + def make(seed: Seed): IO[UserMessage, Entity] + +/** A factory using idGenerator to create the entity */ +class GeneratingFactory[Entity, Seed, Id]( + constructor: Seed => Validated[Id => Entity], + idGenerator: IdGenerator[Id] +) extends EntityFactory[Entity, Seed]: + override def make(seed: Seed): IO[UserMessage, Entity] = + for + construct <- constructor(seed).toZIO + id <- idGenerator.nextId + yield construct(id) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala new file mode 100644 index 0000000..29ae384 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.auth.CurrentUser + +trait EntityUpdateService[Id, Command, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def update(id: Id, command: Command): Op[Unit] + +trait EntityCreateService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(initData: Init): Op[Id] + +trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(id: Id, initData: Init): Op[Id] + +trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] + extends EntityCreateService[Id, Init, Error] + with EntityUpdateService[Id, Command, Error] diff --git a/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala new file mode 100644 index 0000000..946011a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import zio.* + +trait ViewProcessor[Event]: + def process(event: Event): UIO[Unit] diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala new file mode 100644 index 0000000..65c7e92 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* + +/** Represents aggregate root in DDD + * + * @tparam Id + * Aggregate root id + * @tparam Command + * Aggregate root command + * @tparam Event + * Aggregate root event + * @tparam State + * Aggregate root state + */ +trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: + /** Aggregate root id */ + def id: Id + + /** Current state */ + def state: UIO[State] + + def handle(command: Command): IO[Error, Unit] diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala new file mode 100644 index 0000000..c051650 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala @@ -0,0 +1,58 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator + +sealed trait FactoryError: + def entityId: String + def userMessage: UserMessage + +object FactoryError: + case class EntityAlreadyExists[Id](entityId: String, id: Id) + extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) + + case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) + +trait AggregateRootFactory[ + Ident, + T <: AggregateRoot[Ident, ?, ?, ?, ?] +]: + type Id = Ident + type NotFound = FactoryError.EntityNotFound[Id] + type AlreadyExists = FactoryError.EntityAlreadyExists[Id] + + protected def AlreadyExists(id: Id): AlreadyExists = + FactoryError.EntityAlreadyExists(entityId, id) + + protected def NotFound(id: Id): NotFound = + FactoryError.EntityNotFound(entityId, id) + + def entityId: String + def make(id: Id): IO[AlreadyExists, T] + def load(id: Id): IO[NotFound, T] + + def loadOrMake(id: Id): UIO[T] = + load(id) + .orElse(make(id)) + .orDieWith(_ => + new RuntimeException( + s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" + ) + ) + + def make()(using idGen: IdGenerator[Id]): UIO[T] = + val tryToCreate = for + id <- idGen.nextId + entity <- make(id) + yield entity + + tryToCreate + .retryN(10) + .orDieWith(_ => + new RuntimeException( + "Cannot create entity, generator failed 10 times to create new ID" + ) + ) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala new file mode 100644 index 0000000..b2fef3a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala @@ -0,0 +1,24 @@ +package works.iterative.entity + +import works.iterative.core.Validated +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator +import zio.* + +/** Generic class for entity factories. + * + * Creates a new entity from a "seed". + */ +trait EntityFactory[Entity, Seed]: + def make(seed: Seed): IO[UserMessage, Entity] + +/** A factory using idGenerator to create the entity */ +class GeneratingFactory[Entity, Seed, Id]( + constructor: Seed => Validated[Id => Entity], + idGenerator: IdGenerator[Id] +) extends EntityFactory[Entity, Seed]: + override def make(seed: Seed): IO[UserMessage, Entity] = + for + construct <- constructor(seed).toZIO + id <- idGenerator.nextId + yield construct(id) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala new file mode 100644 index 0000000..29ae384 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.auth.CurrentUser + +trait EntityUpdateService[Id, Command, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def update(id: Id, command: Command): Op[Unit] + +trait EntityCreateService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(initData: Init): Op[Id] + +trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(id: Id, initData: Init): Op[Id] + +trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] + extends EntityCreateService[Id, Init, Error] + with EntityUpdateService[Id, Command, Error] diff --git a/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala new file mode 100644 index 0000000..946011a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import zio.* + +trait ViewProcessor[Event]: + def process(event: Event): UIO[Unit] diff --git a/entity/shared/src/main/scala/works/iterative/event/Event.scala b/entity/shared/src/main/scala/works/iterative/event/Event.scala new file mode 100644 index 0000000..7907ac0 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/Event.scala @@ -0,0 +1,4 @@ +package works.iterative.event + +trait Event[A]: + def record: EventRecord diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala new file mode 100644 index 0000000..65c7e92 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* + +/** Represents aggregate root in DDD + * + * @tparam Id + * Aggregate root id + * @tparam Command + * Aggregate root command + * @tparam Event + * Aggregate root event + * @tparam State + * Aggregate root state + */ +trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: + /** Aggregate root id */ + def id: Id + + /** Current state */ + def state: UIO[State] + + def handle(command: Command): IO[Error, Unit] diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala new file mode 100644 index 0000000..c051650 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala @@ -0,0 +1,58 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator + +sealed trait FactoryError: + def entityId: String + def userMessage: UserMessage + +object FactoryError: + case class EntityAlreadyExists[Id](entityId: String, id: Id) + extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) + + case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) + +trait AggregateRootFactory[ + Ident, + T <: AggregateRoot[Ident, ?, ?, ?, ?] +]: + type Id = Ident + type NotFound = FactoryError.EntityNotFound[Id] + type AlreadyExists = FactoryError.EntityAlreadyExists[Id] + + protected def AlreadyExists(id: Id): AlreadyExists = + FactoryError.EntityAlreadyExists(entityId, id) + + protected def NotFound(id: Id): NotFound = + FactoryError.EntityNotFound(entityId, id) + + def entityId: String + def make(id: Id): IO[AlreadyExists, T] + def load(id: Id): IO[NotFound, T] + + def loadOrMake(id: Id): UIO[T] = + load(id) + .orElse(make(id)) + .orDieWith(_ => + new RuntimeException( + s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" + ) + ) + + def make()(using idGen: IdGenerator[Id]): UIO[T] = + val tryToCreate = for + id <- idGen.nextId + entity <- make(id) + yield entity + + tryToCreate + .retryN(10) + .orDieWith(_ => + new RuntimeException( + "Cannot create entity, generator failed 10 times to create new ID" + ) + ) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala new file mode 100644 index 0000000..b2fef3a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala @@ -0,0 +1,24 @@ +package works.iterative.entity + +import works.iterative.core.Validated +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator +import zio.* + +/** Generic class for entity factories. + * + * Creates a new entity from a "seed". + */ +trait EntityFactory[Entity, Seed]: + def make(seed: Seed): IO[UserMessage, Entity] + +/** A factory using idGenerator to create the entity */ +class GeneratingFactory[Entity, Seed, Id]( + constructor: Seed => Validated[Id => Entity], + idGenerator: IdGenerator[Id] +) extends EntityFactory[Entity, Seed]: + override def make(seed: Seed): IO[UserMessage, Entity] = + for + construct <- constructor(seed).toZIO + id <- idGenerator.nextId + yield construct(id) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala new file mode 100644 index 0000000..29ae384 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.auth.CurrentUser + +trait EntityUpdateService[Id, Command, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def update(id: Id, command: Command): Op[Unit] + +trait EntityCreateService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(initData: Init): Op[Id] + +trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(id: Id, initData: Init): Op[Id] + +trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] + extends EntityCreateService[Id, Init, Error] + with EntityUpdateService[Id, Command, Error] diff --git a/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala new file mode 100644 index 0000000..946011a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import zio.* + +trait ViewProcessor[Event]: + def process(event: Event): UIO[Unit] diff --git a/entity/shared/src/main/scala/works/iterative/event/Event.scala b/entity/shared/src/main/scala/works/iterative/event/Event.scala new file mode 100644 index 0000000..7907ac0 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/Event.scala @@ -0,0 +1,4 @@ +package works.iterative.event + +trait Event[A]: + def record: EventRecord diff --git a/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala new file mode 100644 index 0000000..490417a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala @@ -0,0 +1,15 @@ +package works.iterative.event + +import works.iterative.core.* +import zio.* + +/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ +final case class EventRecord(userHandle: UserHandle, timestamp: Moment): + val displayName: String = userHandle.displayName + +object EventRecord: + def apply(userHandle: UserHandle): UIO[EventRecord] = + Moment.now.map(EventRecord(userHandle, _)) + + def now(using userHandle: UserHandle): UIO[EventRecord] = + apply(userHandle) diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala new file mode 100644 index 0000000..65c7e92 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* + +/** Represents aggregate root in DDD + * + * @tparam Id + * Aggregate root id + * @tparam Command + * Aggregate root command + * @tparam Event + * Aggregate root event + * @tparam State + * Aggregate root state + */ +trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: + /** Aggregate root id */ + def id: Id + + /** Current state */ + def state: UIO[State] + + def handle(command: Command): IO[Error, Unit] diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala new file mode 100644 index 0000000..c051650 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala @@ -0,0 +1,58 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator + +sealed trait FactoryError: + def entityId: String + def userMessage: UserMessage + +object FactoryError: + case class EntityAlreadyExists[Id](entityId: String, id: Id) + extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) + + case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) + +trait AggregateRootFactory[ + Ident, + T <: AggregateRoot[Ident, ?, ?, ?, ?] +]: + type Id = Ident + type NotFound = FactoryError.EntityNotFound[Id] + type AlreadyExists = FactoryError.EntityAlreadyExists[Id] + + protected def AlreadyExists(id: Id): AlreadyExists = + FactoryError.EntityAlreadyExists(entityId, id) + + protected def NotFound(id: Id): NotFound = + FactoryError.EntityNotFound(entityId, id) + + def entityId: String + def make(id: Id): IO[AlreadyExists, T] + def load(id: Id): IO[NotFound, T] + + def loadOrMake(id: Id): UIO[T] = + load(id) + .orElse(make(id)) + .orDieWith(_ => + new RuntimeException( + s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" + ) + ) + + def make()(using idGen: IdGenerator[Id]): UIO[T] = + val tryToCreate = for + id <- idGen.nextId + entity <- make(id) + yield entity + + tryToCreate + .retryN(10) + .orDieWith(_ => + new RuntimeException( + "Cannot create entity, generator failed 10 times to create new ID" + ) + ) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala new file mode 100644 index 0000000..b2fef3a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala @@ -0,0 +1,24 @@ +package works.iterative.entity + +import works.iterative.core.Validated +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator +import zio.* + +/** Generic class for entity factories. + * + * Creates a new entity from a "seed". + */ +trait EntityFactory[Entity, Seed]: + def make(seed: Seed): IO[UserMessage, Entity] + +/** A factory using idGenerator to create the entity */ +class GeneratingFactory[Entity, Seed, Id]( + constructor: Seed => Validated[Id => Entity], + idGenerator: IdGenerator[Id] +) extends EntityFactory[Entity, Seed]: + override def make(seed: Seed): IO[UserMessage, Entity] = + for + construct <- constructor(seed).toZIO + id <- idGenerator.nextId + yield construct(id) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala new file mode 100644 index 0000000..29ae384 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.auth.CurrentUser + +trait EntityUpdateService[Id, Command, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def update(id: Id, command: Command): Op[Unit] + +trait EntityCreateService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(initData: Init): Op[Id] + +trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(id: Id, initData: Init): Op[Id] + +trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] + extends EntityCreateService[Id, Init, Error] + with EntityUpdateService[Id, Command, Error] diff --git a/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala new file mode 100644 index 0000000..946011a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import zio.* + +trait ViewProcessor[Event]: + def process(event: Event): UIO[Unit] diff --git a/entity/shared/src/main/scala/works/iterative/event/Event.scala b/entity/shared/src/main/scala/works/iterative/event/Event.scala new file mode 100644 index 0000000..7907ac0 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/Event.scala @@ -0,0 +1,4 @@ +package works.iterative.event + +trait Event[A]: + def record: EventRecord diff --git a/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala new file mode 100644 index 0000000..490417a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala @@ -0,0 +1,15 @@ +package works.iterative.event + +import works.iterative.core.* +import zio.* + +/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ +final case class EventRecord(userHandle: UserHandle, timestamp: Moment): + val displayName: String = userHandle.displayName + +object EventRecord: + def apply(userHandle: UserHandle): UIO[EventRecord] = + Moment.now.map(EventRecord(userHandle, _)) + + def now(using userHandle: UserHandle): UIO[EventRecord] = + apply(userHandle) diff --git a/entity/shared/src/main/scala/works/iterative/event/EventStore.scala b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala new file mode 100644 index 0000000..3226df3 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala @@ -0,0 +1,13 @@ +package works.iterative.event + +import zio.* + +/** Abstraction of event log + * + * The log can persist events and restore the state of an entity from the + * events. + */ +trait EventStore[Id, T <: Event[_]]: + type Op[A] = UIO[A] + def persist(id: Id, event: T): Op[Unit] + def get(id: Id): Op[Seq[T]] diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala new file mode 100644 index 0000000..65c7e92 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* + +/** Represents aggregate root in DDD + * + * @tparam Id + * Aggregate root id + * @tparam Command + * Aggregate root command + * @tparam Event + * Aggregate root event + * @tparam State + * Aggregate root state + */ +trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: + /** Aggregate root id */ + def id: Id + + /** Current state */ + def state: UIO[State] + + def handle(command: Command): IO[Error, Unit] diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala new file mode 100644 index 0000000..c051650 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala @@ -0,0 +1,58 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator + +sealed trait FactoryError: + def entityId: String + def userMessage: UserMessage + +object FactoryError: + case class EntityAlreadyExists[Id](entityId: String, id: Id) + extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) + + case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) + +trait AggregateRootFactory[ + Ident, + T <: AggregateRoot[Ident, ?, ?, ?, ?] +]: + type Id = Ident + type NotFound = FactoryError.EntityNotFound[Id] + type AlreadyExists = FactoryError.EntityAlreadyExists[Id] + + protected def AlreadyExists(id: Id): AlreadyExists = + FactoryError.EntityAlreadyExists(entityId, id) + + protected def NotFound(id: Id): NotFound = + FactoryError.EntityNotFound(entityId, id) + + def entityId: String + def make(id: Id): IO[AlreadyExists, T] + def load(id: Id): IO[NotFound, T] + + def loadOrMake(id: Id): UIO[T] = + load(id) + .orElse(make(id)) + .orDieWith(_ => + new RuntimeException( + s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" + ) + ) + + def make()(using idGen: IdGenerator[Id]): UIO[T] = + val tryToCreate = for + id <- idGen.nextId + entity <- make(id) + yield entity + + tryToCreate + .retryN(10) + .orDieWith(_ => + new RuntimeException( + "Cannot create entity, generator failed 10 times to create new ID" + ) + ) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala new file mode 100644 index 0000000..b2fef3a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala @@ -0,0 +1,24 @@ +package works.iterative.entity + +import works.iterative.core.Validated +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator +import zio.* + +/** Generic class for entity factories. + * + * Creates a new entity from a "seed". + */ +trait EntityFactory[Entity, Seed]: + def make(seed: Seed): IO[UserMessage, Entity] + +/** A factory using idGenerator to create the entity */ +class GeneratingFactory[Entity, Seed, Id]( + constructor: Seed => Validated[Id => Entity], + idGenerator: IdGenerator[Id] +) extends EntityFactory[Entity, Seed]: + override def make(seed: Seed): IO[UserMessage, Entity] = + for + construct <- constructor(seed).toZIO + id <- idGenerator.nextId + yield construct(id) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala new file mode 100644 index 0000000..29ae384 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.auth.CurrentUser + +trait EntityUpdateService[Id, Command, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def update(id: Id, command: Command): Op[Unit] + +trait EntityCreateService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(initData: Init): Op[Id] + +trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(id: Id, initData: Init): Op[Id] + +trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] + extends EntityCreateService[Id, Init, Error] + with EntityUpdateService[Id, Command, Error] diff --git a/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala new file mode 100644 index 0000000..946011a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import zio.* + +trait ViewProcessor[Event]: + def process(event: Event): UIO[Unit] diff --git a/entity/shared/src/main/scala/works/iterative/event/Event.scala b/entity/shared/src/main/scala/works/iterative/event/Event.scala new file mode 100644 index 0000000..7907ac0 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/Event.scala @@ -0,0 +1,4 @@ +package works.iterative.event + +trait Event[A]: + def record: EventRecord diff --git a/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala new file mode 100644 index 0000000..490417a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala @@ -0,0 +1,15 @@ +package works.iterative.event + +import works.iterative.core.* +import zio.* + +/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ +final case class EventRecord(userHandle: UserHandle, timestamp: Moment): + val displayName: String = userHandle.displayName + +object EventRecord: + def apply(userHandle: UserHandle): UIO[EventRecord] = + Moment.now.map(EventRecord(userHandle, _)) + + def now(using userHandle: UserHandle): UIO[EventRecord] = + apply(userHandle) diff --git a/entity/shared/src/main/scala/works/iterative/event/EventStore.scala b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala new file mode 100644 index 0000000..3226df3 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala @@ -0,0 +1,13 @@ +package works.iterative.event + +import zio.* + +/** Abstraction of event log + * + * The log can persist events and restore the state of an entity from the + * events. + */ +trait EventStore[Id, T <: Event[_]]: + type Op[A] = UIO[A] + def persist(id: Id, event: T): Op[Unit] + def get(id: Id): Op[Seq[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala new file mode 100644 index 0000000..6ccccd7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala @@ -0,0 +1,12 @@ +package works.iterative.event + +import zio.* + +/** Snapshot store + * + * Stores a snapshot of an entity. + */ +trait SnapshotStore[Id, T]: + type Op[A] = UIO[A] + def update(id: Id, snapshot: T): Op[Unit] + def get(id: Id): Op[Option[T]] diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala new file mode 100644 index 0000000..65c7e92 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* + +/** Represents aggregate root in DDD + * + * @tparam Id + * Aggregate root id + * @tparam Command + * Aggregate root command + * @tparam Event + * Aggregate root event + * @tparam State + * Aggregate root state + */ +trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: + /** Aggregate root id */ + def id: Id + + /** Current state */ + def state: UIO[State] + + def handle(command: Command): IO[Error, Unit] diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala new file mode 100644 index 0000000..c051650 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala @@ -0,0 +1,58 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator + +sealed trait FactoryError: + def entityId: String + def userMessage: UserMessage + +object FactoryError: + case class EntityAlreadyExists[Id](entityId: String, id: Id) + extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) + + case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) + +trait AggregateRootFactory[ + Ident, + T <: AggregateRoot[Ident, ?, ?, ?, ?] +]: + type Id = Ident + type NotFound = FactoryError.EntityNotFound[Id] + type AlreadyExists = FactoryError.EntityAlreadyExists[Id] + + protected def AlreadyExists(id: Id): AlreadyExists = + FactoryError.EntityAlreadyExists(entityId, id) + + protected def NotFound(id: Id): NotFound = + FactoryError.EntityNotFound(entityId, id) + + def entityId: String + def make(id: Id): IO[AlreadyExists, T] + def load(id: Id): IO[NotFound, T] + + def loadOrMake(id: Id): UIO[T] = + load(id) + .orElse(make(id)) + .orDieWith(_ => + new RuntimeException( + s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" + ) + ) + + def make()(using idGen: IdGenerator[Id]): UIO[T] = + val tryToCreate = for + id <- idGen.nextId + entity <- make(id) + yield entity + + tryToCreate + .retryN(10) + .orDieWith(_ => + new RuntimeException( + "Cannot create entity, generator failed 10 times to create new ID" + ) + ) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala new file mode 100644 index 0000000..b2fef3a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala @@ -0,0 +1,24 @@ +package works.iterative.entity + +import works.iterative.core.Validated +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator +import zio.* + +/** Generic class for entity factories. + * + * Creates a new entity from a "seed". + */ +trait EntityFactory[Entity, Seed]: + def make(seed: Seed): IO[UserMessage, Entity] + +/** A factory using idGenerator to create the entity */ +class GeneratingFactory[Entity, Seed, Id]( + constructor: Seed => Validated[Id => Entity], + idGenerator: IdGenerator[Id] +) extends EntityFactory[Entity, Seed]: + override def make(seed: Seed): IO[UserMessage, Entity] = + for + construct <- constructor(seed).toZIO + id <- idGenerator.nextId + yield construct(id) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala new file mode 100644 index 0000000..29ae384 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.auth.CurrentUser + +trait EntityUpdateService[Id, Command, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def update(id: Id, command: Command): Op[Unit] + +trait EntityCreateService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(initData: Init): Op[Id] + +trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(id: Id, initData: Init): Op[Id] + +trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] + extends EntityCreateService[Id, Init, Error] + with EntityUpdateService[Id, Command, Error] diff --git a/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala new file mode 100644 index 0000000..946011a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import zio.* + +trait ViewProcessor[Event]: + def process(event: Event): UIO[Unit] diff --git a/entity/shared/src/main/scala/works/iterative/event/Event.scala b/entity/shared/src/main/scala/works/iterative/event/Event.scala new file mode 100644 index 0000000..7907ac0 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/Event.scala @@ -0,0 +1,4 @@ +package works.iterative.event + +trait Event[A]: + def record: EventRecord diff --git a/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala new file mode 100644 index 0000000..490417a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala @@ -0,0 +1,15 @@ +package works.iterative.event + +import works.iterative.core.* +import zio.* + +/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ +final case class EventRecord(userHandle: UserHandle, timestamp: Moment): + val displayName: String = userHandle.displayName + +object EventRecord: + def apply(userHandle: UserHandle): UIO[EventRecord] = + Moment.now.map(EventRecord(userHandle, _)) + + def now(using userHandle: UserHandle): UIO[EventRecord] = + apply(userHandle) diff --git a/entity/shared/src/main/scala/works/iterative/event/EventStore.scala b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala new file mode 100644 index 0000000..3226df3 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala @@ -0,0 +1,13 @@ +package works.iterative.event + +import zio.* + +/** Abstraction of event log + * + * The log can persist events and restore the state of an entity from the + * events. + */ +trait EventStore[Id, T <: Event[_]]: + type Op[A] = UIO[A] + def persist(id: Id, event: T): Op[Unit] + def get(id: Id): Op[Seq[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala new file mode 100644 index 0000000..6ccccd7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala @@ -0,0 +1,12 @@ +package works.iterative.event + +import zio.* + +/** Snapshot store + * + * Stores a snapshot of an entity. + */ +trait SnapshotStore[Id, T]: + type Op[A] = UIO[A] + def update(id: Id, snapshot: T): Op[Unit] + def get(id: Id): Op[Option[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala new file mode 100644 index 0000000..964d420 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala @@ -0,0 +1,21 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) + extends EventStore[Id, T]: + override def persist(id: Id, event: T): Op[Unit] = + data.update { m => + val events = m.getOrElse(id, Nil) + m.updated(id, event :: events) + }.unit + override def get(id: Id): Op[Seq[T]] = + data.get.map(_.getOrElse(id, Nil)) + +object InMemoryEventStore: + def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, List[T]]) + yield InMemoryEventStore(data) + } diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala new file mode 100644 index 0000000..65c7e92 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* + +/** Represents aggregate root in DDD + * + * @tparam Id + * Aggregate root id + * @tparam Command + * Aggregate root command + * @tparam Event + * Aggregate root event + * @tparam State + * Aggregate root state + */ +trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: + /** Aggregate root id */ + def id: Id + + /** Current state */ + def state: UIO[State] + + def handle(command: Command): IO[Error, Unit] diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala new file mode 100644 index 0000000..c051650 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala @@ -0,0 +1,58 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator + +sealed trait FactoryError: + def entityId: String + def userMessage: UserMessage + +object FactoryError: + case class EntityAlreadyExists[Id](entityId: String, id: Id) + extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) + + case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) + +trait AggregateRootFactory[ + Ident, + T <: AggregateRoot[Ident, ?, ?, ?, ?] +]: + type Id = Ident + type NotFound = FactoryError.EntityNotFound[Id] + type AlreadyExists = FactoryError.EntityAlreadyExists[Id] + + protected def AlreadyExists(id: Id): AlreadyExists = + FactoryError.EntityAlreadyExists(entityId, id) + + protected def NotFound(id: Id): NotFound = + FactoryError.EntityNotFound(entityId, id) + + def entityId: String + def make(id: Id): IO[AlreadyExists, T] + def load(id: Id): IO[NotFound, T] + + def loadOrMake(id: Id): UIO[T] = + load(id) + .orElse(make(id)) + .orDieWith(_ => + new RuntimeException( + s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" + ) + ) + + def make()(using idGen: IdGenerator[Id]): UIO[T] = + val tryToCreate = for + id <- idGen.nextId + entity <- make(id) + yield entity + + tryToCreate + .retryN(10) + .orDieWith(_ => + new RuntimeException( + "Cannot create entity, generator failed 10 times to create new ID" + ) + ) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala new file mode 100644 index 0000000..b2fef3a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala @@ -0,0 +1,24 @@ +package works.iterative.entity + +import works.iterative.core.Validated +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator +import zio.* + +/** Generic class for entity factories. + * + * Creates a new entity from a "seed". + */ +trait EntityFactory[Entity, Seed]: + def make(seed: Seed): IO[UserMessage, Entity] + +/** A factory using idGenerator to create the entity */ +class GeneratingFactory[Entity, Seed, Id]( + constructor: Seed => Validated[Id => Entity], + idGenerator: IdGenerator[Id] +) extends EntityFactory[Entity, Seed]: + override def make(seed: Seed): IO[UserMessage, Entity] = + for + construct <- constructor(seed).toZIO + id <- idGenerator.nextId + yield construct(id) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala new file mode 100644 index 0000000..29ae384 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.auth.CurrentUser + +trait EntityUpdateService[Id, Command, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def update(id: Id, command: Command): Op[Unit] + +trait EntityCreateService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(initData: Init): Op[Id] + +trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(id: Id, initData: Init): Op[Id] + +trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] + extends EntityCreateService[Id, Init, Error] + with EntityUpdateService[Id, Command, Error] diff --git a/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala new file mode 100644 index 0000000..946011a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import zio.* + +trait ViewProcessor[Event]: + def process(event: Event): UIO[Unit] diff --git a/entity/shared/src/main/scala/works/iterative/event/Event.scala b/entity/shared/src/main/scala/works/iterative/event/Event.scala new file mode 100644 index 0000000..7907ac0 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/Event.scala @@ -0,0 +1,4 @@ +package works.iterative.event + +trait Event[A]: + def record: EventRecord diff --git a/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala new file mode 100644 index 0000000..490417a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala @@ -0,0 +1,15 @@ +package works.iterative.event + +import works.iterative.core.* +import zio.* + +/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ +final case class EventRecord(userHandle: UserHandle, timestamp: Moment): + val displayName: String = userHandle.displayName + +object EventRecord: + def apply(userHandle: UserHandle): UIO[EventRecord] = + Moment.now.map(EventRecord(userHandle, _)) + + def now(using userHandle: UserHandle): UIO[EventRecord] = + apply(userHandle) diff --git a/entity/shared/src/main/scala/works/iterative/event/EventStore.scala b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala new file mode 100644 index 0000000..3226df3 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala @@ -0,0 +1,13 @@ +package works.iterative.event + +import zio.* + +/** Abstraction of event log + * + * The log can persist events and restore the state of an entity from the + * events. + */ +trait EventStore[Id, T <: Event[_]]: + type Op[A] = UIO[A] + def persist(id: Id, event: T): Op[Unit] + def get(id: Id): Op[Seq[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala new file mode 100644 index 0000000..6ccccd7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala @@ -0,0 +1,12 @@ +package works.iterative.event + +import zio.* + +/** Snapshot store + * + * Stores a snapshot of an entity. + */ +trait SnapshotStore[Id, T]: + type Op[A] = UIO[A] + def update(id: Id, snapshot: T): Op[Unit] + def get(id: Id): Op[Option[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala new file mode 100644 index 0000000..964d420 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala @@ -0,0 +1,21 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) + extends EventStore[Id, T]: + override def persist(id: Id, event: T): Op[Unit] = + data.update { m => + val events = m.getOrElse(id, Nil) + m.updated(id, event :: events) + }.unit + override def get(id: Id): Op[Seq[T]] = + data.get.map(_.getOrElse(id, Nil)) + +object InMemoryEventStore: + def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, List[T]]) + yield InMemoryEventStore(data) + } diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala new file mode 100644 index 0000000..722d7d7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala @@ -0,0 +1,19 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) + extends SnapshotStore[Id, T]: + override def update(id: Id, snapshot: T): UIO[Unit] = + data.update(_.updated(id, snapshot)).unit + + override def get(id: Id): UIO[Option[T]] = + data.get.map(_.get(id)) + +object InMemorySnapshotStore: + def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, T]) + yield InMemorySnapshotStore(data) + } diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala new file mode 100644 index 0000000..65c7e92 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* + +/** Represents aggregate root in DDD + * + * @tparam Id + * Aggregate root id + * @tparam Command + * Aggregate root command + * @tparam Event + * Aggregate root event + * @tparam State + * Aggregate root state + */ +trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: + /** Aggregate root id */ + def id: Id + + /** Current state */ + def state: UIO[State] + + def handle(command: Command): IO[Error, Unit] diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala new file mode 100644 index 0000000..c051650 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala @@ -0,0 +1,58 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator + +sealed trait FactoryError: + def entityId: String + def userMessage: UserMessage + +object FactoryError: + case class EntityAlreadyExists[Id](entityId: String, id: Id) + extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) + + case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) + +trait AggregateRootFactory[ + Ident, + T <: AggregateRoot[Ident, ?, ?, ?, ?] +]: + type Id = Ident + type NotFound = FactoryError.EntityNotFound[Id] + type AlreadyExists = FactoryError.EntityAlreadyExists[Id] + + protected def AlreadyExists(id: Id): AlreadyExists = + FactoryError.EntityAlreadyExists(entityId, id) + + protected def NotFound(id: Id): NotFound = + FactoryError.EntityNotFound(entityId, id) + + def entityId: String + def make(id: Id): IO[AlreadyExists, T] + def load(id: Id): IO[NotFound, T] + + def loadOrMake(id: Id): UIO[T] = + load(id) + .orElse(make(id)) + .orDieWith(_ => + new RuntimeException( + s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" + ) + ) + + def make()(using idGen: IdGenerator[Id]): UIO[T] = + val tryToCreate = for + id <- idGen.nextId + entity <- make(id) + yield entity + + tryToCreate + .retryN(10) + .orDieWith(_ => + new RuntimeException( + "Cannot create entity, generator failed 10 times to create new ID" + ) + ) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala new file mode 100644 index 0000000..b2fef3a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala @@ -0,0 +1,24 @@ +package works.iterative.entity + +import works.iterative.core.Validated +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator +import zio.* + +/** Generic class for entity factories. + * + * Creates a new entity from a "seed". + */ +trait EntityFactory[Entity, Seed]: + def make(seed: Seed): IO[UserMessage, Entity] + +/** A factory using idGenerator to create the entity */ +class GeneratingFactory[Entity, Seed, Id]( + constructor: Seed => Validated[Id => Entity], + idGenerator: IdGenerator[Id] +) extends EntityFactory[Entity, Seed]: + override def make(seed: Seed): IO[UserMessage, Entity] = + for + construct <- constructor(seed).toZIO + id <- idGenerator.nextId + yield construct(id) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala new file mode 100644 index 0000000..29ae384 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.auth.CurrentUser + +trait EntityUpdateService[Id, Command, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def update(id: Id, command: Command): Op[Unit] + +trait EntityCreateService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(initData: Init): Op[Id] + +trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(id: Id, initData: Init): Op[Id] + +trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] + extends EntityCreateService[Id, Init, Error] + with EntityUpdateService[Id, Command, Error] diff --git a/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala new file mode 100644 index 0000000..946011a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import zio.* + +trait ViewProcessor[Event]: + def process(event: Event): UIO[Unit] diff --git a/entity/shared/src/main/scala/works/iterative/event/Event.scala b/entity/shared/src/main/scala/works/iterative/event/Event.scala new file mode 100644 index 0000000..7907ac0 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/Event.scala @@ -0,0 +1,4 @@ +package works.iterative.event + +trait Event[A]: + def record: EventRecord diff --git a/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala new file mode 100644 index 0000000..490417a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala @@ -0,0 +1,15 @@ +package works.iterative.event + +import works.iterative.core.* +import zio.* + +/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ +final case class EventRecord(userHandle: UserHandle, timestamp: Moment): + val displayName: String = userHandle.displayName + +object EventRecord: + def apply(userHandle: UserHandle): UIO[EventRecord] = + Moment.now.map(EventRecord(userHandle, _)) + + def now(using userHandle: UserHandle): UIO[EventRecord] = + apply(userHandle) diff --git a/entity/shared/src/main/scala/works/iterative/event/EventStore.scala b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala new file mode 100644 index 0000000..3226df3 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala @@ -0,0 +1,13 @@ +package works.iterative.event + +import zio.* + +/** Abstraction of event log + * + * The log can persist events and restore the state of an entity from the + * events. + */ +trait EventStore[Id, T <: Event[_]]: + type Op[A] = UIO[A] + def persist(id: Id, event: T): Op[Unit] + def get(id: Id): Op[Seq[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala new file mode 100644 index 0000000..6ccccd7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala @@ -0,0 +1,12 @@ +package works.iterative.event + +import zio.* + +/** Snapshot store + * + * Stores a snapshot of an entity. + */ +trait SnapshotStore[Id, T]: + type Op[A] = UIO[A] + def update(id: Id, snapshot: T): Op[Unit] + def get(id: Id): Op[Option[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala new file mode 100644 index 0000000..964d420 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala @@ -0,0 +1,21 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) + extends EventStore[Id, T]: + override def persist(id: Id, event: T): Op[Unit] = + data.update { m => + val events = m.getOrElse(id, Nil) + m.updated(id, event :: events) + }.unit + override def get(id: Id): Op[Seq[T]] = + data.get.map(_.getOrElse(id, Nil)) + +object InMemoryEventStore: + def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, List[T]]) + yield InMemoryEventStore(data) + } diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala new file mode 100644 index 0000000..722d7d7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala @@ -0,0 +1,19 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) + extends SnapshotStore[Id, T]: + override def update(id: Id, snapshot: T): UIO[Unit] = + data.update(_.updated(id, snapshot)).unit + + override def get(id: Id): UIO[Option[T]] = + data.get.map(_.get(id)) + +object InMemorySnapshotStore: + def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, T]) + yield InMemorySnapshotStore(data) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala new file mode 100644 index 0000000..874d16c --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala @@ -0,0 +1,63 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import sttp.client3.* + +class ConsulKeyValueStore( + token: ConsulToken, + baseUri: BaseUri, + backend: CustomTapir.Backend +) extends KeyValueStore[String, String]: + + private val defaultBase = "http://localhost:8500" + private val base = baseUri.value.map(_.toString).getOrElse(defaultBase) + + private def addAuth[U[_], T, R]( + request: RequestT[U, T, R] + ): RequestT[U, T, R] = + request.header("X-Consul-Token", token.token) + + override def get(key: String): UIO[Option[String]] = { + for + response <- addAuth(basicRequest.get(uri"$base/v1/kv/${key}?raw=true")) + .send(backend) + yield response.body.toOption + }.orDie + + override def put(key: String, value: String): UIO[Unit] = { + addAuth(basicRequest.put(uri"$base/v1/kv/${key}").body(value)) + .send(backend) + .unit + }.orDie + + override def remove(key: String): UIO[Unit] = { + addAuth(basicRequest.delete(uri"$base/v1/kv/${key}")) + .send(backend) + .unit + }.orDie + +object ConsulKeyValueStore: + val layer: URLayer[ + ConsulToken & BaseUri & CustomTapir.Backend, + StringKeyValueStore + ] = + ZLayer { + for + token <- ZIO.service[ConsulToken] + baseUri <- ZIO.service[BaseUri] + backend <- ZIO.service[CustomTapir.Backend] + yield ConsulKeyValueStore(token, baseUri, backend) + } + + val fromEnv + : ZLayer[CustomTapir.Backend, SecurityException, StringKeyValueStore] = + ConsulToken.fromEnv >>> ZLayer { + for + token <- ZIO.service[ConsulToken] + backend <- ZIO.service[CustomTapir.Backend] + addr <- System.envOrElse("CONSUL_ADDR", "http://localhost:8500") + yield ConsulKeyValueStore(token, BaseUri(addr), backend) + } diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala new file mode 100644 index 0000000..65c7e92 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* + +/** Represents aggregate root in DDD + * + * @tparam Id + * Aggregate root id + * @tparam Command + * Aggregate root command + * @tparam Event + * Aggregate root event + * @tparam State + * Aggregate root state + */ +trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: + /** Aggregate root id */ + def id: Id + + /** Current state */ + def state: UIO[State] + + def handle(command: Command): IO[Error, Unit] diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala new file mode 100644 index 0000000..c051650 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala @@ -0,0 +1,58 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator + +sealed trait FactoryError: + def entityId: String + def userMessage: UserMessage + +object FactoryError: + case class EntityAlreadyExists[Id](entityId: String, id: Id) + extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) + + case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) + +trait AggregateRootFactory[ + Ident, + T <: AggregateRoot[Ident, ?, ?, ?, ?] +]: + type Id = Ident + type NotFound = FactoryError.EntityNotFound[Id] + type AlreadyExists = FactoryError.EntityAlreadyExists[Id] + + protected def AlreadyExists(id: Id): AlreadyExists = + FactoryError.EntityAlreadyExists(entityId, id) + + protected def NotFound(id: Id): NotFound = + FactoryError.EntityNotFound(entityId, id) + + def entityId: String + def make(id: Id): IO[AlreadyExists, T] + def load(id: Id): IO[NotFound, T] + + def loadOrMake(id: Id): UIO[T] = + load(id) + .orElse(make(id)) + .orDieWith(_ => + new RuntimeException( + s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" + ) + ) + + def make()(using idGen: IdGenerator[Id]): UIO[T] = + val tryToCreate = for + id <- idGen.nextId + entity <- make(id) + yield entity + + tryToCreate + .retryN(10) + .orDieWith(_ => + new RuntimeException( + "Cannot create entity, generator failed 10 times to create new ID" + ) + ) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala new file mode 100644 index 0000000..b2fef3a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala @@ -0,0 +1,24 @@ +package works.iterative.entity + +import works.iterative.core.Validated +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator +import zio.* + +/** Generic class for entity factories. + * + * Creates a new entity from a "seed". + */ +trait EntityFactory[Entity, Seed]: + def make(seed: Seed): IO[UserMessage, Entity] + +/** A factory using idGenerator to create the entity */ +class GeneratingFactory[Entity, Seed, Id]( + constructor: Seed => Validated[Id => Entity], + idGenerator: IdGenerator[Id] +) extends EntityFactory[Entity, Seed]: + override def make(seed: Seed): IO[UserMessage, Entity] = + for + construct <- constructor(seed).toZIO + id <- idGenerator.nextId + yield construct(id) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala new file mode 100644 index 0000000..29ae384 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.auth.CurrentUser + +trait EntityUpdateService[Id, Command, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def update(id: Id, command: Command): Op[Unit] + +trait EntityCreateService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(initData: Init): Op[Id] + +trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(id: Id, initData: Init): Op[Id] + +trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] + extends EntityCreateService[Id, Init, Error] + with EntityUpdateService[Id, Command, Error] diff --git a/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala new file mode 100644 index 0000000..946011a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import zio.* + +trait ViewProcessor[Event]: + def process(event: Event): UIO[Unit] diff --git a/entity/shared/src/main/scala/works/iterative/event/Event.scala b/entity/shared/src/main/scala/works/iterative/event/Event.scala new file mode 100644 index 0000000..7907ac0 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/Event.scala @@ -0,0 +1,4 @@ +package works.iterative.event + +trait Event[A]: + def record: EventRecord diff --git a/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala new file mode 100644 index 0000000..490417a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala @@ -0,0 +1,15 @@ +package works.iterative.event + +import works.iterative.core.* +import zio.* + +/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ +final case class EventRecord(userHandle: UserHandle, timestamp: Moment): + val displayName: String = userHandle.displayName + +object EventRecord: + def apply(userHandle: UserHandle): UIO[EventRecord] = + Moment.now.map(EventRecord(userHandle, _)) + + def now(using userHandle: UserHandle): UIO[EventRecord] = + apply(userHandle) diff --git a/entity/shared/src/main/scala/works/iterative/event/EventStore.scala b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala new file mode 100644 index 0000000..3226df3 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala @@ -0,0 +1,13 @@ +package works.iterative.event + +import zio.* + +/** Abstraction of event log + * + * The log can persist events and restore the state of an entity from the + * events. + */ +trait EventStore[Id, T <: Event[_]]: + type Op[A] = UIO[A] + def persist(id: Id, event: T): Op[Unit] + def get(id: Id): Op[Seq[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala new file mode 100644 index 0000000..6ccccd7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala @@ -0,0 +1,12 @@ +package works.iterative.event + +import zio.* + +/** Snapshot store + * + * Stores a snapshot of an entity. + */ +trait SnapshotStore[Id, T]: + type Op[A] = UIO[A] + def update(id: Id, snapshot: T): Op[Unit] + def get(id: Id): Op[Option[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala new file mode 100644 index 0000000..964d420 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala @@ -0,0 +1,21 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) + extends EventStore[Id, T]: + override def persist(id: Id, event: T): Op[Unit] = + data.update { m => + val events = m.getOrElse(id, Nil) + m.updated(id, event :: events) + }.unit + override def get(id: Id): Op[Seq[T]] = + data.get.map(_.getOrElse(id, Nil)) + +object InMemoryEventStore: + def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, List[T]]) + yield InMemoryEventStore(data) + } diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala new file mode 100644 index 0000000..722d7d7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala @@ -0,0 +1,19 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) + extends SnapshotStore[Id, T]: + override def update(id: Id, snapshot: T): UIO[Unit] = + data.update(_.updated(id, snapshot)).unit + + override def get(id: Id): UIO[Option[T]] = + data.get.map(_.get(id)) + +object InMemorySnapshotStore: + def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, T]) + yield InMemorySnapshotStore(data) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala new file mode 100644 index 0000000..874d16c --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala @@ -0,0 +1,63 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import sttp.client3.* + +class ConsulKeyValueStore( + token: ConsulToken, + baseUri: BaseUri, + backend: CustomTapir.Backend +) extends KeyValueStore[String, String]: + + private val defaultBase = "http://localhost:8500" + private val base = baseUri.value.map(_.toString).getOrElse(defaultBase) + + private def addAuth[U[_], T, R]( + request: RequestT[U, T, R] + ): RequestT[U, T, R] = + request.header("X-Consul-Token", token.token) + + override def get(key: String): UIO[Option[String]] = { + for + response <- addAuth(basicRequest.get(uri"$base/v1/kv/${key}?raw=true")) + .send(backend) + yield response.body.toOption + }.orDie + + override def put(key: String, value: String): UIO[Unit] = { + addAuth(basicRequest.put(uri"$base/v1/kv/${key}").body(value)) + .send(backend) + .unit + }.orDie + + override def remove(key: String): UIO[Unit] = { + addAuth(basicRequest.delete(uri"$base/v1/kv/${key}")) + .send(backend) + .unit + }.orDie + +object ConsulKeyValueStore: + val layer: URLayer[ + ConsulToken & BaseUri & CustomTapir.Backend, + StringKeyValueStore + ] = + ZLayer { + for + token <- ZIO.service[ConsulToken] + baseUri <- ZIO.service[BaseUri] + backend <- ZIO.service[CustomTapir.Backend] + yield ConsulKeyValueStore(token, baseUri, backend) + } + + val fromEnv + : ZLayer[CustomTapir.Backend, SecurityException, StringKeyValueStore] = + ConsulToken.fromEnv >>> ZLayer { + for + token <- ZIO.service[ConsulToken] + backend <- ZIO.service[CustomTapir.Backend] + addr <- System.envOrElse("CONSUL_ADDR", "http://localhost:8500") + yield ConsulKeyValueStore(token, BaseUri(addr), backend) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala new file mode 100644 index 0000000..5c5483a --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala @@ -0,0 +1,150 @@ +package works.iterative.core.service.impl + +import zio.* +import sttp.tapir.EndpointIO.annotations.* +import sttp.tapir.Schema +import zio.json.* + +final case class ConsulToken(token: Option[String]) + +object ConsulToken: + def fromEnv: ZLayer[Any, SecurityException, ConsulToken] = ZLayer { + System.env("CONSUL_TOKEN").map(ConsulToken(_)) + } + +@jsonMemberNames(PascalCase) +final case class ConsulMetadata( + createIndex: Long, + modifyIndex: Long, + lockIndex: Long, + key: String, + flags: Long, + value: String, + session: Option[String] +) derives JsonCodec, + Schema + +@endpointInput("v1/kv/{key}") +final case class GetQuery( + @path + @description("Key to get from the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + // TODO: This would not work type-wise properly, as the result type of the call would differ depending on the query parameters + // These are missing, split to different queries to enable. + /* + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies the response is just the raw value of the key, without any encoding or metadata." + ) + raw: Option[Boolean] = None, + @query + @description( + "Specifies to return only keys (no values or metadata). Specifying this parameter implies recurse." + ) + keys: Option[Boolean] = None, + @query + @description( + "Specifies the string to use as a separator for recursive key lookups. This option is only used when paired with the keys parameter to limit the prefix of keys returned, only up to the given separator." + ) + separator: Option[String] = None, + */ + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class PutQuery( + @path + @description("Key to put into the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies an unsigned value between 0 and (2^64)-1 to store with the key. API consumers can use this field any way they choose for their application." + ) + flags: Option[Long] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Supply a session ID to use in a lock acquisition operation.") + acquire: Option[String] = None, + @query + @description("Supply a session ID to use in a release operation.") + release: Option[String] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class DeleteQuery( + @path + @description("Key to delete in the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +/* +sealed trait ConsulGetError +object ConsulGetError: + case object NotFound extends ConsulGetError derives JsonCodec, Schema + case object Forbidden extends ConsulGetError derives JsonCodec, Schema + case class ServerError(msg: String) extends ConsulGetError derives JsonCodec, Schema + +trait ConsulKVEndpoints extends CustomTapir: + private val kvBase = endpoint + .in("v1" / "kv") + .securityIn( + auth.apiKey(header[Option[String]]("X-Consul-Token").mapTo[ConsulToken]) + ) + + val getKV: Endpoint[ConsulToken, GetQuery, Unit, ConsulMetadata, Any] = + kvBase.get + .in(EndpointInput.derived[GetQuery]) + .out(jsonBody[ConsulMetadata]) + .errorOut( + oneOf[ConsulGetError]( + oneOfVariant( + statusCode(StatusCode.NotFound).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfVariant( + statusCode(StatusCode.Forbidden).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfDefaultVariant( + statusCode(StatusCode.InternalServerError).and(jsonBody[ServerError].description("not found")) + ) + ) + ) + val putKV: Endpoint[ConsulToken, PutQuery, Unit, Boolean, Any] = + kvBase.put.in(EndpointInput.derived[PutQuery]).out(jsonBody[Boolean]) + val deleteKV: Endpoint[ConsulToken, DeleteQuery, Unit, Boolean, Any] = + kvBase.delete.in(EndpointInput.derived[DeleteQuery]).out(jsonBody[Boolean]) + +*/ \ No newline at end of file diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala new file mode 100644 index 0000000..65c7e92 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* + +/** Represents aggregate root in DDD + * + * @tparam Id + * Aggregate root id + * @tparam Command + * Aggregate root command + * @tparam Event + * Aggregate root event + * @tparam State + * Aggregate root state + */ +trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: + /** Aggregate root id */ + def id: Id + + /** Current state */ + def state: UIO[State] + + def handle(command: Command): IO[Error, Unit] diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala new file mode 100644 index 0000000..c051650 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala @@ -0,0 +1,58 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator + +sealed trait FactoryError: + def entityId: String + def userMessage: UserMessage + +object FactoryError: + case class EntityAlreadyExists[Id](entityId: String, id: Id) + extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) + + case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) + +trait AggregateRootFactory[ + Ident, + T <: AggregateRoot[Ident, ?, ?, ?, ?] +]: + type Id = Ident + type NotFound = FactoryError.EntityNotFound[Id] + type AlreadyExists = FactoryError.EntityAlreadyExists[Id] + + protected def AlreadyExists(id: Id): AlreadyExists = + FactoryError.EntityAlreadyExists(entityId, id) + + protected def NotFound(id: Id): NotFound = + FactoryError.EntityNotFound(entityId, id) + + def entityId: String + def make(id: Id): IO[AlreadyExists, T] + def load(id: Id): IO[NotFound, T] + + def loadOrMake(id: Id): UIO[T] = + load(id) + .orElse(make(id)) + .orDieWith(_ => + new RuntimeException( + s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" + ) + ) + + def make()(using idGen: IdGenerator[Id]): UIO[T] = + val tryToCreate = for + id <- idGen.nextId + entity <- make(id) + yield entity + + tryToCreate + .retryN(10) + .orDieWith(_ => + new RuntimeException( + "Cannot create entity, generator failed 10 times to create new ID" + ) + ) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala new file mode 100644 index 0000000..b2fef3a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala @@ -0,0 +1,24 @@ +package works.iterative.entity + +import works.iterative.core.Validated +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator +import zio.* + +/** Generic class for entity factories. + * + * Creates a new entity from a "seed". + */ +trait EntityFactory[Entity, Seed]: + def make(seed: Seed): IO[UserMessage, Entity] + +/** A factory using idGenerator to create the entity */ +class GeneratingFactory[Entity, Seed, Id]( + constructor: Seed => Validated[Id => Entity], + idGenerator: IdGenerator[Id] +) extends EntityFactory[Entity, Seed]: + override def make(seed: Seed): IO[UserMessage, Entity] = + for + construct <- constructor(seed).toZIO + id <- idGenerator.nextId + yield construct(id) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala new file mode 100644 index 0000000..29ae384 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.auth.CurrentUser + +trait EntityUpdateService[Id, Command, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def update(id: Id, command: Command): Op[Unit] + +trait EntityCreateService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(initData: Init): Op[Id] + +trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(id: Id, initData: Init): Op[Id] + +trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] + extends EntityCreateService[Id, Init, Error] + with EntityUpdateService[Id, Command, Error] diff --git a/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala new file mode 100644 index 0000000..946011a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import zio.* + +trait ViewProcessor[Event]: + def process(event: Event): UIO[Unit] diff --git a/entity/shared/src/main/scala/works/iterative/event/Event.scala b/entity/shared/src/main/scala/works/iterative/event/Event.scala new file mode 100644 index 0000000..7907ac0 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/Event.scala @@ -0,0 +1,4 @@ +package works.iterative.event + +trait Event[A]: + def record: EventRecord diff --git a/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala new file mode 100644 index 0000000..490417a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala @@ -0,0 +1,15 @@ +package works.iterative.event + +import works.iterative.core.* +import zio.* + +/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ +final case class EventRecord(userHandle: UserHandle, timestamp: Moment): + val displayName: String = userHandle.displayName + +object EventRecord: + def apply(userHandle: UserHandle): UIO[EventRecord] = + Moment.now.map(EventRecord(userHandle, _)) + + def now(using userHandle: UserHandle): UIO[EventRecord] = + apply(userHandle) diff --git a/entity/shared/src/main/scala/works/iterative/event/EventStore.scala b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala new file mode 100644 index 0000000..3226df3 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala @@ -0,0 +1,13 @@ +package works.iterative.event + +import zio.* + +/** Abstraction of event log + * + * The log can persist events and restore the state of an entity from the + * events. + */ +trait EventStore[Id, T <: Event[_]]: + type Op[A] = UIO[A] + def persist(id: Id, event: T): Op[Unit] + def get(id: Id): Op[Seq[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala new file mode 100644 index 0000000..6ccccd7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala @@ -0,0 +1,12 @@ +package works.iterative.event + +import zio.* + +/** Snapshot store + * + * Stores a snapshot of an entity. + */ +trait SnapshotStore[Id, T]: + type Op[A] = UIO[A] + def update(id: Id, snapshot: T): Op[Unit] + def get(id: Id): Op[Option[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala new file mode 100644 index 0000000..964d420 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala @@ -0,0 +1,21 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) + extends EventStore[Id, T]: + override def persist(id: Id, event: T): Op[Unit] = + data.update { m => + val events = m.getOrElse(id, Nil) + m.updated(id, event :: events) + }.unit + override def get(id: Id): Op[Seq[T]] = + data.get.map(_.getOrElse(id, Nil)) + +object InMemoryEventStore: + def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, List[T]]) + yield InMemoryEventStore(data) + } diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala new file mode 100644 index 0000000..722d7d7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala @@ -0,0 +1,19 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) + extends SnapshotStore[Id, T]: + override def update(id: Id, snapshot: T): UIO[Unit] = + data.update(_.updated(id, snapshot)).unit + + override def get(id: Id): UIO[Option[T]] = + data.get.map(_.get(id)) + +object InMemorySnapshotStore: + def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, T]) + yield InMemorySnapshotStore(data) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala new file mode 100644 index 0000000..874d16c --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala @@ -0,0 +1,63 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import sttp.client3.* + +class ConsulKeyValueStore( + token: ConsulToken, + baseUri: BaseUri, + backend: CustomTapir.Backend +) extends KeyValueStore[String, String]: + + private val defaultBase = "http://localhost:8500" + private val base = baseUri.value.map(_.toString).getOrElse(defaultBase) + + private def addAuth[U[_], T, R]( + request: RequestT[U, T, R] + ): RequestT[U, T, R] = + request.header("X-Consul-Token", token.token) + + override def get(key: String): UIO[Option[String]] = { + for + response <- addAuth(basicRequest.get(uri"$base/v1/kv/${key}?raw=true")) + .send(backend) + yield response.body.toOption + }.orDie + + override def put(key: String, value: String): UIO[Unit] = { + addAuth(basicRequest.put(uri"$base/v1/kv/${key}").body(value)) + .send(backend) + .unit + }.orDie + + override def remove(key: String): UIO[Unit] = { + addAuth(basicRequest.delete(uri"$base/v1/kv/${key}")) + .send(backend) + .unit + }.orDie + +object ConsulKeyValueStore: + val layer: URLayer[ + ConsulToken & BaseUri & CustomTapir.Backend, + StringKeyValueStore + ] = + ZLayer { + for + token <- ZIO.service[ConsulToken] + baseUri <- ZIO.service[BaseUri] + backend <- ZIO.service[CustomTapir.Backend] + yield ConsulKeyValueStore(token, baseUri, backend) + } + + val fromEnv + : ZLayer[CustomTapir.Backend, SecurityException, StringKeyValueStore] = + ConsulToken.fromEnv >>> ZLayer { + for + token <- ZIO.service[ConsulToken] + backend <- ZIO.service[CustomTapir.Backend] + addr <- System.envOrElse("CONSUL_ADDR", "http://localhost:8500") + yield ConsulKeyValueStore(token, BaseUri(addr), backend) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala new file mode 100644 index 0000000..5c5483a --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala @@ -0,0 +1,150 @@ +package works.iterative.core.service.impl + +import zio.* +import sttp.tapir.EndpointIO.annotations.* +import sttp.tapir.Schema +import zio.json.* + +final case class ConsulToken(token: Option[String]) + +object ConsulToken: + def fromEnv: ZLayer[Any, SecurityException, ConsulToken] = ZLayer { + System.env("CONSUL_TOKEN").map(ConsulToken(_)) + } + +@jsonMemberNames(PascalCase) +final case class ConsulMetadata( + createIndex: Long, + modifyIndex: Long, + lockIndex: Long, + key: String, + flags: Long, + value: String, + session: Option[String] +) derives JsonCodec, + Schema + +@endpointInput("v1/kv/{key}") +final case class GetQuery( + @path + @description("Key to get from the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + // TODO: This would not work type-wise properly, as the result type of the call would differ depending on the query parameters + // These are missing, split to different queries to enable. + /* + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies the response is just the raw value of the key, without any encoding or metadata." + ) + raw: Option[Boolean] = None, + @query + @description( + "Specifies to return only keys (no values or metadata). Specifying this parameter implies recurse." + ) + keys: Option[Boolean] = None, + @query + @description( + "Specifies the string to use as a separator for recursive key lookups. This option is only used when paired with the keys parameter to limit the prefix of keys returned, only up to the given separator." + ) + separator: Option[String] = None, + */ + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class PutQuery( + @path + @description("Key to put into the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies an unsigned value between 0 and (2^64)-1 to store with the key. API consumers can use this field any way they choose for their application." + ) + flags: Option[Long] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Supply a session ID to use in a lock acquisition operation.") + acquire: Option[String] = None, + @query + @description("Supply a session ID to use in a release operation.") + release: Option[String] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class DeleteQuery( + @path + @description("Key to delete in the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +/* +sealed trait ConsulGetError +object ConsulGetError: + case object NotFound extends ConsulGetError derives JsonCodec, Schema + case object Forbidden extends ConsulGetError derives JsonCodec, Schema + case class ServerError(msg: String) extends ConsulGetError derives JsonCodec, Schema + +trait ConsulKVEndpoints extends CustomTapir: + private val kvBase = endpoint + .in("v1" / "kv") + .securityIn( + auth.apiKey(header[Option[String]]("X-Consul-Token").mapTo[ConsulToken]) + ) + + val getKV: Endpoint[ConsulToken, GetQuery, Unit, ConsulMetadata, Any] = + kvBase.get + .in(EndpointInput.derived[GetQuery]) + .out(jsonBody[ConsulMetadata]) + .errorOut( + oneOf[ConsulGetError]( + oneOfVariant( + statusCode(StatusCode.NotFound).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfVariant( + statusCode(StatusCode.Forbidden).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfDefaultVariant( + statusCode(StatusCode.InternalServerError).and(jsonBody[ServerError].description("not found")) + ) + ) + ) + val putKV: Endpoint[ConsulToken, PutQuery, Unit, Boolean, Any] = + kvBase.put.in(EndpointInput.derived[PutQuery]).out(jsonBody[Boolean]) + val deleteKV: Endpoint[ConsulToken, DeleteQuery, Unit, Boolean, Any] = + kvBase.delete.in(EndpointInput.derived[DeleteQuery]).out(jsonBody[Boolean]) + +*/ \ No newline at end of file diff --git a/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala new file mode 100644 index 0000000..c938083 --- /dev/null +++ b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala @@ -0,0 +1,19 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import zio.test.* + +object ConsulKeyValueStoreSpec extends specs.KeyValueStoreSpec: + override def spec = defaultSpec.provide( + ConsulKeyValueStore.layer, + CustomTapir.clientLayer, + ZLayer { + Live.live(System.env("CONSUL_ADDR")).map(a => BaseUri(a.get)) + }, + ZLayer { + Live.live(System.env("CONSUL_TOKEN")).map(ConsulToken(_)) + } + ) @@ TestAspect.ifEnvSet("CONSUL_ADDR") diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala new file mode 100644 index 0000000..65c7e92 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* + +/** Represents aggregate root in DDD + * + * @tparam Id + * Aggregate root id + * @tparam Command + * Aggregate root command + * @tparam Event + * Aggregate root event + * @tparam State + * Aggregate root state + */ +trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: + /** Aggregate root id */ + def id: Id + + /** Current state */ + def state: UIO[State] + + def handle(command: Command): IO[Error, Unit] diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala new file mode 100644 index 0000000..c051650 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala @@ -0,0 +1,58 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator + +sealed trait FactoryError: + def entityId: String + def userMessage: UserMessage + +object FactoryError: + case class EntityAlreadyExists[Id](entityId: String, id: Id) + extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) + + case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) + +trait AggregateRootFactory[ + Ident, + T <: AggregateRoot[Ident, ?, ?, ?, ?] +]: + type Id = Ident + type NotFound = FactoryError.EntityNotFound[Id] + type AlreadyExists = FactoryError.EntityAlreadyExists[Id] + + protected def AlreadyExists(id: Id): AlreadyExists = + FactoryError.EntityAlreadyExists(entityId, id) + + protected def NotFound(id: Id): NotFound = + FactoryError.EntityNotFound(entityId, id) + + def entityId: String + def make(id: Id): IO[AlreadyExists, T] + def load(id: Id): IO[NotFound, T] + + def loadOrMake(id: Id): UIO[T] = + load(id) + .orElse(make(id)) + .orDieWith(_ => + new RuntimeException( + s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" + ) + ) + + def make()(using idGen: IdGenerator[Id]): UIO[T] = + val tryToCreate = for + id <- idGen.nextId + entity <- make(id) + yield entity + + tryToCreate + .retryN(10) + .orDieWith(_ => + new RuntimeException( + "Cannot create entity, generator failed 10 times to create new ID" + ) + ) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala new file mode 100644 index 0000000..b2fef3a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala @@ -0,0 +1,24 @@ +package works.iterative.entity + +import works.iterative.core.Validated +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator +import zio.* + +/** Generic class for entity factories. + * + * Creates a new entity from a "seed". + */ +trait EntityFactory[Entity, Seed]: + def make(seed: Seed): IO[UserMessage, Entity] + +/** A factory using idGenerator to create the entity */ +class GeneratingFactory[Entity, Seed, Id]( + constructor: Seed => Validated[Id => Entity], + idGenerator: IdGenerator[Id] +) extends EntityFactory[Entity, Seed]: + override def make(seed: Seed): IO[UserMessage, Entity] = + for + construct <- constructor(seed).toZIO + id <- idGenerator.nextId + yield construct(id) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala new file mode 100644 index 0000000..29ae384 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.auth.CurrentUser + +trait EntityUpdateService[Id, Command, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def update(id: Id, command: Command): Op[Unit] + +trait EntityCreateService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(initData: Init): Op[Id] + +trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(id: Id, initData: Init): Op[Id] + +trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] + extends EntityCreateService[Id, Init, Error] + with EntityUpdateService[Id, Command, Error] diff --git a/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala new file mode 100644 index 0000000..946011a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import zio.* + +trait ViewProcessor[Event]: + def process(event: Event): UIO[Unit] diff --git a/entity/shared/src/main/scala/works/iterative/event/Event.scala b/entity/shared/src/main/scala/works/iterative/event/Event.scala new file mode 100644 index 0000000..7907ac0 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/Event.scala @@ -0,0 +1,4 @@ +package works.iterative.event + +trait Event[A]: + def record: EventRecord diff --git a/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala new file mode 100644 index 0000000..490417a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala @@ -0,0 +1,15 @@ +package works.iterative.event + +import works.iterative.core.* +import zio.* + +/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ +final case class EventRecord(userHandle: UserHandle, timestamp: Moment): + val displayName: String = userHandle.displayName + +object EventRecord: + def apply(userHandle: UserHandle): UIO[EventRecord] = + Moment.now.map(EventRecord(userHandle, _)) + + def now(using userHandle: UserHandle): UIO[EventRecord] = + apply(userHandle) diff --git a/entity/shared/src/main/scala/works/iterative/event/EventStore.scala b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala new file mode 100644 index 0000000..3226df3 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala @@ -0,0 +1,13 @@ +package works.iterative.event + +import zio.* + +/** Abstraction of event log + * + * The log can persist events and restore the state of an entity from the + * events. + */ +trait EventStore[Id, T <: Event[_]]: + type Op[A] = UIO[A] + def persist(id: Id, event: T): Op[Unit] + def get(id: Id): Op[Seq[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala new file mode 100644 index 0000000..6ccccd7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala @@ -0,0 +1,12 @@ +package works.iterative.event + +import zio.* + +/** Snapshot store + * + * Stores a snapshot of an entity. + */ +trait SnapshotStore[Id, T]: + type Op[A] = UIO[A] + def update(id: Id, snapshot: T): Op[Unit] + def get(id: Id): Op[Option[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala new file mode 100644 index 0000000..964d420 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala @@ -0,0 +1,21 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) + extends EventStore[Id, T]: + override def persist(id: Id, event: T): Op[Unit] = + data.update { m => + val events = m.getOrElse(id, Nil) + m.updated(id, event :: events) + }.unit + override def get(id: Id): Op[Seq[T]] = + data.get.map(_.getOrElse(id, Nil)) + +object InMemoryEventStore: + def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, List[T]]) + yield InMemoryEventStore(data) + } diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala new file mode 100644 index 0000000..722d7d7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala @@ -0,0 +1,19 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) + extends SnapshotStore[Id, T]: + override def update(id: Id, snapshot: T): UIO[Unit] = + data.update(_.updated(id, snapshot)).unit + + override def get(id: Id): UIO[Option[T]] = + data.get.map(_.get(id)) + +object InMemorySnapshotStore: + def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, T]) + yield InMemorySnapshotStore(data) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala new file mode 100644 index 0000000..874d16c --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala @@ -0,0 +1,63 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import sttp.client3.* + +class ConsulKeyValueStore( + token: ConsulToken, + baseUri: BaseUri, + backend: CustomTapir.Backend +) extends KeyValueStore[String, String]: + + private val defaultBase = "http://localhost:8500" + private val base = baseUri.value.map(_.toString).getOrElse(defaultBase) + + private def addAuth[U[_], T, R]( + request: RequestT[U, T, R] + ): RequestT[U, T, R] = + request.header("X-Consul-Token", token.token) + + override def get(key: String): UIO[Option[String]] = { + for + response <- addAuth(basicRequest.get(uri"$base/v1/kv/${key}?raw=true")) + .send(backend) + yield response.body.toOption + }.orDie + + override def put(key: String, value: String): UIO[Unit] = { + addAuth(basicRequest.put(uri"$base/v1/kv/${key}").body(value)) + .send(backend) + .unit + }.orDie + + override def remove(key: String): UIO[Unit] = { + addAuth(basicRequest.delete(uri"$base/v1/kv/${key}")) + .send(backend) + .unit + }.orDie + +object ConsulKeyValueStore: + val layer: URLayer[ + ConsulToken & BaseUri & CustomTapir.Backend, + StringKeyValueStore + ] = + ZLayer { + for + token <- ZIO.service[ConsulToken] + baseUri <- ZIO.service[BaseUri] + backend <- ZIO.service[CustomTapir.Backend] + yield ConsulKeyValueStore(token, baseUri, backend) + } + + val fromEnv + : ZLayer[CustomTapir.Backend, SecurityException, StringKeyValueStore] = + ConsulToken.fromEnv >>> ZLayer { + for + token <- ZIO.service[ConsulToken] + backend <- ZIO.service[CustomTapir.Backend] + addr <- System.envOrElse("CONSUL_ADDR", "http://localhost:8500") + yield ConsulKeyValueStore(token, BaseUri(addr), backend) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala new file mode 100644 index 0000000..5c5483a --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala @@ -0,0 +1,150 @@ +package works.iterative.core.service.impl + +import zio.* +import sttp.tapir.EndpointIO.annotations.* +import sttp.tapir.Schema +import zio.json.* + +final case class ConsulToken(token: Option[String]) + +object ConsulToken: + def fromEnv: ZLayer[Any, SecurityException, ConsulToken] = ZLayer { + System.env("CONSUL_TOKEN").map(ConsulToken(_)) + } + +@jsonMemberNames(PascalCase) +final case class ConsulMetadata( + createIndex: Long, + modifyIndex: Long, + lockIndex: Long, + key: String, + flags: Long, + value: String, + session: Option[String] +) derives JsonCodec, + Schema + +@endpointInput("v1/kv/{key}") +final case class GetQuery( + @path + @description("Key to get from the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + // TODO: This would not work type-wise properly, as the result type of the call would differ depending on the query parameters + // These are missing, split to different queries to enable. + /* + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies the response is just the raw value of the key, without any encoding or metadata." + ) + raw: Option[Boolean] = None, + @query + @description( + "Specifies to return only keys (no values or metadata). Specifying this parameter implies recurse." + ) + keys: Option[Boolean] = None, + @query + @description( + "Specifies the string to use as a separator for recursive key lookups. This option is only used when paired with the keys parameter to limit the prefix of keys returned, only up to the given separator." + ) + separator: Option[String] = None, + */ + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class PutQuery( + @path + @description("Key to put into the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies an unsigned value between 0 and (2^64)-1 to store with the key. API consumers can use this field any way they choose for their application." + ) + flags: Option[Long] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Supply a session ID to use in a lock acquisition operation.") + acquire: Option[String] = None, + @query + @description("Supply a session ID to use in a release operation.") + release: Option[String] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class DeleteQuery( + @path + @description("Key to delete in the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +/* +sealed trait ConsulGetError +object ConsulGetError: + case object NotFound extends ConsulGetError derives JsonCodec, Schema + case object Forbidden extends ConsulGetError derives JsonCodec, Schema + case class ServerError(msg: String) extends ConsulGetError derives JsonCodec, Schema + +trait ConsulKVEndpoints extends CustomTapir: + private val kvBase = endpoint + .in("v1" / "kv") + .securityIn( + auth.apiKey(header[Option[String]]("X-Consul-Token").mapTo[ConsulToken]) + ) + + val getKV: Endpoint[ConsulToken, GetQuery, Unit, ConsulMetadata, Any] = + kvBase.get + .in(EndpointInput.derived[GetQuery]) + .out(jsonBody[ConsulMetadata]) + .errorOut( + oneOf[ConsulGetError]( + oneOfVariant( + statusCode(StatusCode.NotFound).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfVariant( + statusCode(StatusCode.Forbidden).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfDefaultVariant( + statusCode(StatusCode.InternalServerError).and(jsonBody[ServerError].description("not found")) + ) + ) + ) + val putKV: Endpoint[ConsulToken, PutQuery, Unit, Boolean, Any] = + kvBase.put.in(EndpointInput.derived[PutQuery]).out(jsonBody[Boolean]) + val deleteKV: Endpoint[ConsulToken, DeleteQuery, Unit, Boolean, Any] = + kvBase.delete.in(EndpointInput.derived[DeleteQuery]).out(jsonBody[Boolean]) + +*/ \ No newline at end of file diff --git a/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala new file mode 100644 index 0000000..c938083 --- /dev/null +++ b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala @@ -0,0 +1,19 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import zio.test.* + +object ConsulKeyValueStoreSpec extends specs.KeyValueStoreSpec: + override def spec = defaultSpec.provide( + ConsulKeyValueStore.layer, + CustomTapir.clientLayer, + ZLayer { + Live.live(System.env("CONSUL_ADDR")).map(a => BaseUri(a.get)) + }, + ZLayer { + Live.live(System.env("CONSUL_TOKEN")).map(ConsulToken(_)) + } + ) @@ TestAspect.ifEnvSet("CONSUL_ADDR") diff --git a/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala new file mode 100644 index 0000000..16211a8 --- /dev/null +++ b/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala @@ -0,0 +1,29 @@ +package works.iterative.core.service.impl + +import org.scalajs.dom.Storage +import works.iterative.core.service.Repository +import zio.* +import zio.json.* + +// TODO: improve error reporting on generic repositories +// This is good just for prototypes +// And it cannot be used to query data, as there is no way to iterate the storage +class JsStorageRepository[Value: JsonCodec]( + storage: Storage +) extends Repository[String, Value, String]: + + override def load(id: String): UIO[Option[Value]] = { + for + raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) + data <- ZIO.foreach(raw) { r => + ZIO + .fromEither(r.fromJson[Value]) + .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) + } + yield data + }.orDie + + override def save(key: String, value: Value): UIO[Unit] = + ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie + + override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala new file mode 100644 index 0000000..65c7e92 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* + +/** Represents aggregate root in DDD + * + * @tparam Id + * Aggregate root id + * @tparam Command + * Aggregate root command + * @tparam Event + * Aggregate root event + * @tparam State + * Aggregate root state + */ +trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: + /** Aggregate root id */ + def id: Id + + /** Current state */ + def state: UIO[State] + + def handle(command: Command): IO[Error, Unit] diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala new file mode 100644 index 0000000..c051650 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala @@ -0,0 +1,58 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator + +sealed trait FactoryError: + def entityId: String + def userMessage: UserMessage + +object FactoryError: + case class EntityAlreadyExists[Id](entityId: String, id: Id) + extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) + + case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) + +trait AggregateRootFactory[ + Ident, + T <: AggregateRoot[Ident, ?, ?, ?, ?] +]: + type Id = Ident + type NotFound = FactoryError.EntityNotFound[Id] + type AlreadyExists = FactoryError.EntityAlreadyExists[Id] + + protected def AlreadyExists(id: Id): AlreadyExists = + FactoryError.EntityAlreadyExists(entityId, id) + + protected def NotFound(id: Id): NotFound = + FactoryError.EntityNotFound(entityId, id) + + def entityId: String + def make(id: Id): IO[AlreadyExists, T] + def load(id: Id): IO[NotFound, T] + + def loadOrMake(id: Id): UIO[T] = + load(id) + .orElse(make(id)) + .orDieWith(_ => + new RuntimeException( + s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" + ) + ) + + def make()(using idGen: IdGenerator[Id]): UIO[T] = + val tryToCreate = for + id <- idGen.nextId + entity <- make(id) + yield entity + + tryToCreate + .retryN(10) + .orDieWith(_ => + new RuntimeException( + "Cannot create entity, generator failed 10 times to create new ID" + ) + ) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala new file mode 100644 index 0000000..b2fef3a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala @@ -0,0 +1,24 @@ +package works.iterative.entity + +import works.iterative.core.Validated +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator +import zio.* + +/** Generic class for entity factories. + * + * Creates a new entity from a "seed". + */ +trait EntityFactory[Entity, Seed]: + def make(seed: Seed): IO[UserMessage, Entity] + +/** A factory using idGenerator to create the entity */ +class GeneratingFactory[Entity, Seed, Id]( + constructor: Seed => Validated[Id => Entity], + idGenerator: IdGenerator[Id] +) extends EntityFactory[Entity, Seed]: + override def make(seed: Seed): IO[UserMessage, Entity] = + for + construct <- constructor(seed).toZIO + id <- idGenerator.nextId + yield construct(id) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala new file mode 100644 index 0000000..29ae384 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.auth.CurrentUser + +trait EntityUpdateService[Id, Command, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def update(id: Id, command: Command): Op[Unit] + +trait EntityCreateService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(initData: Init): Op[Id] + +trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(id: Id, initData: Init): Op[Id] + +trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] + extends EntityCreateService[Id, Init, Error] + with EntityUpdateService[Id, Command, Error] diff --git a/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala new file mode 100644 index 0000000..946011a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import zio.* + +trait ViewProcessor[Event]: + def process(event: Event): UIO[Unit] diff --git a/entity/shared/src/main/scala/works/iterative/event/Event.scala b/entity/shared/src/main/scala/works/iterative/event/Event.scala new file mode 100644 index 0000000..7907ac0 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/Event.scala @@ -0,0 +1,4 @@ +package works.iterative.event + +trait Event[A]: + def record: EventRecord diff --git a/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala new file mode 100644 index 0000000..490417a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala @@ -0,0 +1,15 @@ +package works.iterative.event + +import works.iterative.core.* +import zio.* + +/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ +final case class EventRecord(userHandle: UserHandle, timestamp: Moment): + val displayName: String = userHandle.displayName + +object EventRecord: + def apply(userHandle: UserHandle): UIO[EventRecord] = + Moment.now.map(EventRecord(userHandle, _)) + + def now(using userHandle: UserHandle): UIO[EventRecord] = + apply(userHandle) diff --git a/entity/shared/src/main/scala/works/iterative/event/EventStore.scala b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala new file mode 100644 index 0000000..3226df3 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala @@ -0,0 +1,13 @@ +package works.iterative.event + +import zio.* + +/** Abstraction of event log + * + * The log can persist events and restore the state of an entity from the + * events. + */ +trait EventStore[Id, T <: Event[_]]: + type Op[A] = UIO[A] + def persist(id: Id, event: T): Op[Unit] + def get(id: Id): Op[Seq[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala new file mode 100644 index 0000000..6ccccd7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala @@ -0,0 +1,12 @@ +package works.iterative.event + +import zio.* + +/** Snapshot store + * + * Stores a snapshot of an entity. + */ +trait SnapshotStore[Id, T]: + type Op[A] = UIO[A] + def update(id: Id, snapshot: T): Op[Unit] + def get(id: Id): Op[Option[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala new file mode 100644 index 0000000..964d420 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala @@ -0,0 +1,21 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) + extends EventStore[Id, T]: + override def persist(id: Id, event: T): Op[Unit] = + data.update { m => + val events = m.getOrElse(id, Nil) + m.updated(id, event :: events) + }.unit + override def get(id: Id): Op[Seq[T]] = + data.get.map(_.getOrElse(id, Nil)) + +object InMemoryEventStore: + def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, List[T]]) + yield InMemoryEventStore(data) + } diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala new file mode 100644 index 0000000..722d7d7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala @@ -0,0 +1,19 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) + extends SnapshotStore[Id, T]: + override def update(id: Id, snapshot: T): UIO[Unit] = + data.update(_.updated(id, snapshot)).unit + + override def get(id: Id): UIO[Option[T]] = + data.get.map(_.get(id)) + +object InMemorySnapshotStore: + def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, T]) + yield InMemorySnapshotStore(data) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala new file mode 100644 index 0000000..874d16c --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala @@ -0,0 +1,63 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import sttp.client3.* + +class ConsulKeyValueStore( + token: ConsulToken, + baseUri: BaseUri, + backend: CustomTapir.Backend +) extends KeyValueStore[String, String]: + + private val defaultBase = "http://localhost:8500" + private val base = baseUri.value.map(_.toString).getOrElse(defaultBase) + + private def addAuth[U[_], T, R]( + request: RequestT[U, T, R] + ): RequestT[U, T, R] = + request.header("X-Consul-Token", token.token) + + override def get(key: String): UIO[Option[String]] = { + for + response <- addAuth(basicRequest.get(uri"$base/v1/kv/${key}?raw=true")) + .send(backend) + yield response.body.toOption + }.orDie + + override def put(key: String, value: String): UIO[Unit] = { + addAuth(basicRequest.put(uri"$base/v1/kv/${key}").body(value)) + .send(backend) + .unit + }.orDie + + override def remove(key: String): UIO[Unit] = { + addAuth(basicRequest.delete(uri"$base/v1/kv/${key}")) + .send(backend) + .unit + }.orDie + +object ConsulKeyValueStore: + val layer: URLayer[ + ConsulToken & BaseUri & CustomTapir.Backend, + StringKeyValueStore + ] = + ZLayer { + for + token <- ZIO.service[ConsulToken] + baseUri <- ZIO.service[BaseUri] + backend <- ZIO.service[CustomTapir.Backend] + yield ConsulKeyValueStore(token, baseUri, backend) + } + + val fromEnv + : ZLayer[CustomTapir.Backend, SecurityException, StringKeyValueStore] = + ConsulToken.fromEnv >>> ZLayer { + for + token <- ZIO.service[ConsulToken] + backend <- ZIO.service[CustomTapir.Backend] + addr <- System.envOrElse("CONSUL_ADDR", "http://localhost:8500") + yield ConsulKeyValueStore(token, BaseUri(addr), backend) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala new file mode 100644 index 0000000..5c5483a --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala @@ -0,0 +1,150 @@ +package works.iterative.core.service.impl + +import zio.* +import sttp.tapir.EndpointIO.annotations.* +import sttp.tapir.Schema +import zio.json.* + +final case class ConsulToken(token: Option[String]) + +object ConsulToken: + def fromEnv: ZLayer[Any, SecurityException, ConsulToken] = ZLayer { + System.env("CONSUL_TOKEN").map(ConsulToken(_)) + } + +@jsonMemberNames(PascalCase) +final case class ConsulMetadata( + createIndex: Long, + modifyIndex: Long, + lockIndex: Long, + key: String, + flags: Long, + value: String, + session: Option[String] +) derives JsonCodec, + Schema + +@endpointInput("v1/kv/{key}") +final case class GetQuery( + @path + @description("Key to get from the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + // TODO: This would not work type-wise properly, as the result type of the call would differ depending on the query parameters + // These are missing, split to different queries to enable. + /* + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies the response is just the raw value of the key, without any encoding or metadata." + ) + raw: Option[Boolean] = None, + @query + @description( + "Specifies to return only keys (no values or metadata). Specifying this parameter implies recurse." + ) + keys: Option[Boolean] = None, + @query + @description( + "Specifies the string to use as a separator for recursive key lookups. This option is only used when paired with the keys parameter to limit the prefix of keys returned, only up to the given separator." + ) + separator: Option[String] = None, + */ + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class PutQuery( + @path + @description("Key to put into the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies an unsigned value between 0 and (2^64)-1 to store with the key. API consumers can use this field any way they choose for their application." + ) + flags: Option[Long] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Supply a session ID to use in a lock acquisition operation.") + acquire: Option[String] = None, + @query + @description("Supply a session ID to use in a release operation.") + release: Option[String] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class DeleteQuery( + @path + @description("Key to delete in the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +/* +sealed trait ConsulGetError +object ConsulGetError: + case object NotFound extends ConsulGetError derives JsonCodec, Schema + case object Forbidden extends ConsulGetError derives JsonCodec, Schema + case class ServerError(msg: String) extends ConsulGetError derives JsonCodec, Schema + +trait ConsulKVEndpoints extends CustomTapir: + private val kvBase = endpoint + .in("v1" / "kv") + .securityIn( + auth.apiKey(header[Option[String]]("X-Consul-Token").mapTo[ConsulToken]) + ) + + val getKV: Endpoint[ConsulToken, GetQuery, Unit, ConsulMetadata, Any] = + kvBase.get + .in(EndpointInput.derived[GetQuery]) + .out(jsonBody[ConsulMetadata]) + .errorOut( + oneOf[ConsulGetError]( + oneOfVariant( + statusCode(StatusCode.NotFound).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfVariant( + statusCode(StatusCode.Forbidden).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfDefaultVariant( + statusCode(StatusCode.InternalServerError).and(jsonBody[ServerError].description("not found")) + ) + ) + ) + val putKV: Endpoint[ConsulToken, PutQuery, Unit, Boolean, Any] = + kvBase.put.in(EndpointInput.derived[PutQuery]).out(jsonBody[Boolean]) + val deleteKV: Endpoint[ConsulToken, DeleteQuery, Unit, Boolean, Any] = + kvBase.delete.in(EndpointInput.derived[DeleteQuery]).out(jsonBody[Boolean]) + +*/ \ No newline at end of file diff --git a/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala new file mode 100644 index 0000000..c938083 --- /dev/null +++ b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala @@ -0,0 +1,19 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import zio.test.* + +object ConsulKeyValueStoreSpec extends specs.KeyValueStoreSpec: + override def spec = defaultSpec.provide( + ConsulKeyValueStore.layer, + CustomTapir.clientLayer, + ZLayer { + Live.live(System.env("CONSUL_ADDR")).map(a => BaseUri(a.get)) + }, + ZLayer { + Live.live(System.env("CONSUL_TOKEN")).map(ConsulToken(_)) + } + ) @@ TestAspect.ifEnvSet("CONSUL_ADDR") diff --git a/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala new file mode 100644 index 0000000..16211a8 --- /dev/null +++ b/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala @@ -0,0 +1,29 @@ +package works.iterative.core.service.impl + +import org.scalajs.dom.Storage +import works.iterative.core.service.Repository +import zio.* +import zio.json.* + +// TODO: improve error reporting on generic repositories +// This is good just for prototypes +// And it cannot be used to query data, as there is no way to iterate the storage +class JsStorageRepository[Value: JsonCodec]( + storage: Storage +) extends Repository[String, Value, String]: + + override def load(id: String): UIO[Option[Value]] = { + for + raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) + data <- ZIO.foreach(raw) { r => + ZIO + .fromEither(r.fromJson[Value]) + .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) + } + yield data + }.orDie + + override def save(key: String, value: Value): UIO[Unit] = + ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie + + override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala new file mode 100644 index 0000000..8c88818 --- /dev/null +++ b/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala @@ -0,0 +1,26 @@ +package works.iterative.core +package service +package impl + +import zio.* +import zio.stream.* + +class JcaDigestService extends DigestService: + + override def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): IO[E, Digest] = + for + md <- ZIO + .attempt( + java.security.MessageDigest.getInstance(algorithm.value) + ) + .orDie + _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) + digest <- ZIO.attempt(md.digest()).orDie + yield Digest(algorithm, digest) + +object JcaDigestGenerator: + val layer: ULayer[DigestService] = + ZLayer.succeed(new JcaDigestService) diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala new file mode 100644 index 0000000..65c7e92 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* + +/** Represents aggregate root in DDD + * + * @tparam Id + * Aggregate root id + * @tparam Command + * Aggregate root command + * @tparam Event + * Aggregate root event + * @tparam State + * Aggregate root state + */ +trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: + /** Aggregate root id */ + def id: Id + + /** Current state */ + def state: UIO[State] + + def handle(command: Command): IO[Error, Unit] diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala new file mode 100644 index 0000000..c051650 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala @@ -0,0 +1,58 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator + +sealed trait FactoryError: + def entityId: String + def userMessage: UserMessage + +object FactoryError: + case class EntityAlreadyExists[Id](entityId: String, id: Id) + extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) + + case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) + +trait AggregateRootFactory[ + Ident, + T <: AggregateRoot[Ident, ?, ?, ?, ?] +]: + type Id = Ident + type NotFound = FactoryError.EntityNotFound[Id] + type AlreadyExists = FactoryError.EntityAlreadyExists[Id] + + protected def AlreadyExists(id: Id): AlreadyExists = + FactoryError.EntityAlreadyExists(entityId, id) + + protected def NotFound(id: Id): NotFound = + FactoryError.EntityNotFound(entityId, id) + + def entityId: String + def make(id: Id): IO[AlreadyExists, T] + def load(id: Id): IO[NotFound, T] + + def loadOrMake(id: Id): UIO[T] = + load(id) + .orElse(make(id)) + .orDieWith(_ => + new RuntimeException( + s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" + ) + ) + + def make()(using idGen: IdGenerator[Id]): UIO[T] = + val tryToCreate = for + id <- idGen.nextId + entity <- make(id) + yield entity + + tryToCreate + .retryN(10) + .orDieWith(_ => + new RuntimeException( + "Cannot create entity, generator failed 10 times to create new ID" + ) + ) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala new file mode 100644 index 0000000..b2fef3a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala @@ -0,0 +1,24 @@ +package works.iterative.entity + +import works.iterative.core.Validated +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator +import zio.* + +/** Generic class for entity factories. + * + * Creates a new entity from a "seed". + */ +trait EntityFactory[Entity, Seed]: + def make(seed: Seed): IO[UserMessage, Entity] + +/** A factory using idGenerator to create the entity */ +class GeneratingFactory[Entity, Seed, Id]( + constructor: Seed => Validated[Id => Entity], + idGenerator: IdGenerator[Id] +) extends EntityFactory[Entity, Seed]: + override def make(seed: Seed): IO[UserMessage, Entity] = + for + construct <- constructor(seed).toZIO + id <- idGenerator.nextId + yield construct(id) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala new file mode 100644 index 0000000..29ae384 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.auth.CurrentUser + +trait EntityUpdateService[Id, Command, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def update(id: Id, command: Command): Op[Unit] + +trait EntityCreateService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(initData: Init): Op[Id] + +trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(id: Id, initData: Init): Op[Id] + +trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] + extends EntityCreateService[Id, Init, Error] + with EntityUpdateService[Id, Command, Error] diff --git a/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala new file mode 100644 index 0000000..946011a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import zio.* + +trait ViewProcessor[Event]: + def process(event: Event): UIO[Unit] diff --git a/entity/shared/src/main/scala/works/iterative/event/Event.scala b/entity/shared/src/main/scala/works/iterative/event/Event.scala new file mode 100644 index 0000000..7907ac0 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/Event.scala @@ -0,0 +1,4 @@ +package works.iterative.event + +trait Event[A]: + def record: EventRecord diff --git a/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala new file mode 100644 index 0000000..490417a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala @@ -0,0 +1,15 @@ +package works.iterative.event + +import works.iterative.core.* +import zio.* + +/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ +final case class EventRecord(userHandle: UserHandle, timestamp: Moment): + val displayName: String = userHandle.displayName + +object EventRecord: + def apply(userHandle: UserHandle): UIO[EventRecord] = + Moment.now.map(EventRecord(userHandle, _)) + + def now(using userHandle: UserHandle): UIO[EventRecord] = + apply(userHandle) diff --git a/entity/shared/src/main/scala/works/iterative/event/EventStore.scala b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala new file mode 100644 index 0000000..3226df3 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala @@ -0,0 +1,13 @@ +package works.iterative.event + +import zio.* + +/** Abstraction of event log + * + * The log can persist events and restore the state of an entity from the + * events. + */ +trait EventStore[Id, T <: Event[_]]: + type Op[A] = UIO[A] + def persist(id: Id, event: T): Op[Unit] + def get(id: Id): Op[Seq[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala new file mode 100644 index 0000000..6ccccd7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala @@ -0,0 +1,12 @@ +package works.iterative.event + +import zio.* + +/** Snapshot store + * + * Stores a snapshot of an entity. + */ +trait SnapshotStore[Id, T]: + type Op[A] = UIO[A] + def update(id: Id, snapshot: T): Op[Unit] + def get(id: Id): Op[Option[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala new file mode 100644 index 0000000..964d420 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala @@ -0,0 +1,21 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) + extends EventStore[Id, T]: + override def persist(id: Id, event: T): Op[Unit] = + data.update { m => + val events = m.getOrElse(id, Nil) + m.updated(id, event :: events) + }.unit + override def get(id: Id): Op[Seq[T]] = + data.get.map(_.getOrElse(id, Nil)) + +object InMemoryEventStore: + def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, List[T]]) + yield InMemoryEventStore(data) + } diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala new file mode 100644 index 0000000..722d7d7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala @@ -0,0 +1,19 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) + extends SnapshotStore[Id, T]: + override def update(id: Id, snapshot: T): UIO[Unit] = + data.update(_.updated(id, snapshot)).unit + + override def get(id: Id): UIO[Option[T]] = + data.get.map(_.get(id)) + +object InMemorySnapshotStore: + def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, T]) + yield InMemorySnapshotStore(data) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala new file mode 100644 index 0000000..874d16c --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala @@ -0,0 +1,63 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import sttp.client3.* + +class ConsulKeyValueStore( + token: ConsulToken, + baseUri: BaseUri, + backend: CustomTapir.Backend +) extends KeyValueStore[String, String]: + + private val defaultBase = "http://localhost:8500" + private val base = baseUri.value.map(_.toString).getOrElse(defaultBase) + + private def addAuth[U[_], T, R]( + request: RequestT[U, T, R] + ): RequestT[U, T, R] = + request.header("X-Consul-Token", token.token) + + override def get(key: String): UIO[Option[String]] = { + for + response <- addAuth(basicRequest.get(uri"$base/v1/kv/${key}?raw=true")) + .send(backend) + yield response.body.toOption + }.orDie + + override def put(key: String, value: String): UIO[Unit] = { + addAuth(basicRequest.put(uri"$base/v1/kv/${key}").body(value)) + .send(backend) + .unit + }.orDie + + override def remove(key: String): UIO[Unit] = { + addAuth(basicRequest.delete(uri"$base/v1/kv/${key}")) + .send(backend) + .unit + }.orDie + +object ConsulKeyValueStore: + val layer: URLayer[ + ConsulToken & BaseUri & CustomTapir.Backend, + StringKeyValueStore + ] = + ZLayer { + for + token <- ZIO.service[ConsulToken] + baseUri <- ZIO.service[BaseUri] + backend <- ZIO.service[CustomTapir.Backend] + yield ConsulKeyValueStore(token, baseUri, backend) + } + + val fromEnv + : ZLayer[CustomTapir.Backend, SecurityException, StringKeyValueStore] = + ConsulToken.fromEnv >>> ZLayer { + for + token <- ZIO.service[ConsulToken] + backend <- ZIO.service[CustomTapir.Backend] + addr <- System.envOrElse("CONSUL_ADDR", "http://localhost:8500") + yield ConsulKeyValueStore(token, BaseUri(addr), backend) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala new file mode 100644 index 0000000..5c5483a --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala @@ -0,0 +1,150 @@ +package works.iterative.core.service.impl + +import zio.* +import sttp.tapir.EndpointIO.annotations.* +import sttp.tapir.Schema +import zio.json.* + +final case class ConsulToken(token: Option[String]) + +object ConsulToken: + def fromEnv: ZLayer[Any, SecurityException, ConsulToken] = ZLayer { + System.env("CONSUL_TOKEN").map(ConsulToken(_)) + } + +@jsonMemberNames(PascalCase) +final case class ConsulMetadata( + createIndex: Long, + modifyIndex: Long, + lockIndex: Long, + key: String, + flags: Long, + value: String, + session: Option[String] +) derives JsonCodec, + Schema + +@endpointInput("v1/kv/{key}") +final case class GetQuery( + @path + @description("Key to get from the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + // TODO: This would not work type-wise properly, as the result type of the call would differ depending on the query parameters + // These are missing, split to different queries to enable. + /* + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies the response is just the raw value of the key, without any encoding or metadata." + ) + raw: Option[Boolean] = None, + @query + @description( + "Specifies to return only keys (no values or metadata). Specifying this parameter implies recurse." + ) + keys: Option[Boolean] = None, + @query + @description( + "Specifies the string to use as a separator for recursive key lookups. This option is only used when paired with the keys parameter to limit the prefix of keys returned, only up to the given separator." + ) + separator: Option[String] = None, + */ + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class PutQuery( + @path + @description("Key to put into the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies an unsigned value between 0 and (2^64)-1 to store with the key. API consumers can use this field any way they choose for their application." + ) + flags: Option[Long] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Supply a session ID to use in a lock acquisition operation.") + acquire: Option[String] = None, + @query + @description("Supply a session ID to use in a release operation.") + release: Option[String] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class DeleteQuery( + @path + @description("Key to delete in the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +/* +sealed trait ConsulGetError +object ConsulGetError: + case object NotFound extends ConsulGetError derives JsonCodec, Schema + case object Forbidden extends ConsulGetError derives JsonCodec, Schema + case class ServerError(msg: String) extends ConsulGetError derives JsonCodec, Schema + +trait ConsulKVEndpoints extends CustomTapir: + private val kvBase = endpoint + .in("v1" / "kv") + .securityIn( + auth.apiKey(header[Option[String]]("X-Consul-Token").mapTo[ConsulToken]) + ) + + val getKV: Endpoint[ConsulToken, GetQuery, Unit, ConsulMetadata, Any] = + kvBase.get + .in(EndpointInput.derived[GetQuery]) + .out(jsonBody[ConsulMetadata]) + .errorOut( + oneOf[ConsulGetError]( + oneOfVariant( + statusCode(StatusCode.NotFound).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfVariant( + statusCode(StatusCode.Forbidden).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfDefaultVariant( + statusCode(StatusCode.InternalServerError).and(jsonBody[ServerError].description("not found")) + ) + ) + ) + val putKV: Endpoint[ConsulToken, PutQuery, Unit, Boolean, Any] = + kvBase.put.in(EndpointInput.derived[PutQuery]).out(jsonBody[Boolean]) + val deleteKV: Endpoint[ConsulToken, DeleteQuery, Unit, Boolean, Any] = + kvBase.delete.in(EndpointInput.derived[DeleteQuery]).out(jsonBody[Boolean]) + +*/ \ No newline at end of file diff --git a/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala new file mode 100644 index 0000000..c938083 --- /dev/null +++ b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala @@ -0,0 +1,19 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import zio.test.* + +object ConsulKeyValueStoreSpec extends specs.KeyValueStoreSpec: + override def spec = defaultSpec.provide( + ConsulKeyValueStore.layer, + CustomTapir.clientLayer, + ZLayer { + Live.live(System.env("CONSUL_ADDR")).map(a => BaseUri(a.get)) + }, + ZLayer { + Live.live(System.env("CONSUL_TOKEN")).map(ConsulToken(_)) + } + ) @@ TestAspect.ifEnvSet("CONSUL_ADDR") diff --git a/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala new file mode 100644 index 0000000..16211a8 --- /dev/null +++ b/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala @@ -0,0 +1,29 @@ +package works.iterative.core.service.impl + +import org.scalajs.dom.Storage +import works.iterative.core.service.Repository +import zio.* +import zio.json.* + +// TODO: improve error reporting on generic repositories +// This is good just for prototypes +// And it cannot be used to query data, as there is no way to iterate the storage +class JsStorageRepository[Value: JsonCodec]( + storage: Storage +) extends Repository[String, Value, String]: + + override def load(id: String): UIO[Option[Value]] = { + for + raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) + data <- ZIO.foreach(raw) { r => + ZIO + .fromEither(r.fromJson[Value]) + .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) + } + yield data + }.orDie + + override def save(key: String, value: Value): UIO[Unit] = + ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie + + override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala new file mode 100644 index 0000000..8c88818 --- /dev/null +++ b/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala @@ -0,0 +1,26 @@ +package works.iterative.core +package service +package impl + +import zio.* +import zio.stream.* + +class JcaDigestService extends DigestService: + + override def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): IO[E, Digest] = + for + md <- ZIO + .attempt( + java.security.MessageDigest.getInstance(algorithm.value) + ) + .orDie + _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) + digest <- ZIO.attempt(md.digest()).orDie + yield Digest(algorithm, digest) + +object JcaDigestGenerator: + val layer: ULayer[DigestService] = + ZLayer.succeed(new JcaDigestService) diff --git a/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala new file mode 100644 index 0000000..883bd7a --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala @@ -0,0 +1,26 @@ +package works.iterative.core.service + +import works.iterative.core.Digest +import works.iterative.core.DigestAlgorithm + +import zio.* +import zio.stream.* + +trait DigestService: + def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): IO[E, Digest] + +object DigestService: + def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): ZIO[DigestService, E, Digest] = + ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) + + def digest( + algorithm: DigestAlgorithm, + content: Array[Byte] + ): URIO[DigestService, Digest] = + digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala new file mode 100644 index 0000000..65c7e92 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* + +/** Represents aggregate root in DDD + * + * @tparam Id + * Aggregate root id + * @tparam Command + * Aggregate root command + * @tparam Event + * Aggregate root event + * @tparam State + * Aggregate root state + */ +trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: + /** Aggregate root id */ + def id: Id + + /** Current state */ + def state: UIO[State] + + def handle(command: Command): IO[Error, Unit] diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala new file mode 100644 index 0000000..c051650 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala @@ -0,0 +1,58 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator + +sealed trait FactoryError: + def entityId: String + def userMessage: UserMessage + +object FactoryError: + case class EntityAlreadyExists[Id](entityId: String, id: Id) + extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) + + case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) + +trait AggregateRootFactory[ + Ident, + T <: AggregateRoot[Ident, ?, ?, ?, ?] +]: + type Id = Ident + type NotFound = FactoryError.EntityNotFound[Id] + type AlreadyExists = FactoryError.EntityAlreadyExists[Id] + + protected def AlreadyExists(id: Id): AlreadyExists = + FactoryError.EntityAlreadyExists(entityId, id) + + protected def NotFound(id: Id): NotFound = + FactoryError.EntityNotFound(entityId, id) + + def entityId: String + def make(id: Id): IO[AlreadyExists, T] + def load(id: Id): IO[NotFound, T] + + def loadOrMake(id: Id): UIO[T] = + load(id) + .orElse(make(id)) + .orDieWith(_ => + new RuntimeException( + s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" + ) + ) + + def make()(using idGen: IdGenerator[Id]): UIO[T] = + val tryToCreate = for + id <- idGen.nextId + entity <- make(id) + yield entity + + tryToCreate + .retryN(10) + .orDieWith(_ => + new RuntimeException( + "Cannot create entity, generator failed 10 times to create new ID" + ) + ) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala new file mode 100644 index 0000000..b2fef3a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala @@ -0,0 +1,24 @@ +package works.iterative.entity + +import works.iterative.core.Validated +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator +import zio.* + +/** Generic class for entity factories. + * + * Creates a new entity from a "seed". + */ +trait EntityFactory[Entity, Seed]: + def make(seed: Seed): IO[UserMessage, Entity] + +/** A factory using idGenerator to create the entity */ +class GeneratingFactory[Entity, Seed, Id]( + constructor: Seed => Validated[Id => Entity], + idGenerator: IdGenerator[Id] +) extends EntityFactory[Entity, Seed]: + override def make(seed: Seed): IO[UserMessage, Entity] = + for + construct <- constructor(seed).toZIO + id <- idGenerator.nextId + yield construct(id) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala new file mode 100644 index 0000000..29ae384 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.auth.CurrentUser + +trait EntityUpdateService[Id, Command, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def update(id: Id, command: Command): Op[Unit] + +trait EntityCreateService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(initData: Init): Op[Id] + +trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(id: Id, initData: Init): Op[Id] + +trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] + extends EntityCreateService[Id, Init, Error] + with EntityUpdateService[Id, Command, Error] diff --git a/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala new file mode 100644 index 0000000..946011a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import zio.* + +trait ViewProcessor[Event]: + def process(event: Event): UIO[Unit] diff --git a/entity/shared/src/main/scala/works/iterative/event/Event.scala b/entity/shared/src/main/scala/works/iterative/event/Event.scala new file mode 100644 index 0000000..7907ac0 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/Event.scala @@ -0,0 +1,4 @@ +package works.iterative.event + +trait Event[A]: + def record: EventRecord diff --git a/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala new file mode 100644 index 0000000..490417a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala @@ -0,0 +1,15 @@ +package works.iterative.event + +import works.iterative.core.* +import zio.* + +/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ +final case class EventRecord(userHandle: UserHandle, timestamp: Moment): + val displayName: String = userHandle.displayName + +object EventRecord: + def apply(userHandle: UserHandle): UIO[EventRecord] = + Moment.now.map(EventRecord(userHandle, _)) + + def now(using userHandle: UserHandle): UIO[EventRecord] = + apply(userHandle) diff --git a/entity/shared/src/main/scala/works/iterative/event/EventStore.scala b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala new file mode 100644 index 0000000..3226df3 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala @@ -0,0 +1,13 @@ +package works.iterative.event + +import zio.* + +/** Abstraction of event log + * + * The log can persist events and restore the state of an entity from the + * events. + */ +trait EventStore[Id, T <: Event[_]]: + type Op[A] = UIO[A] + def persist(id: Id, event: T): Op[Unit] + def get(id: Id): Op[Seq[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala new file mode 100644 index 0000000..6ccccd7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala @@ -0,0 +1,12 @@ +package works.iterative.event + +import zio.* + +/** Snapshot store + * + * Stores a snapshot of an entity. + */ +trait SnapshotStore[Id, T]: + type Op[A] = UIO[A] + def update(id: Id, snapshot: T): Op[Unit] + def get(id: Id): Op[Option[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala new file mode 100644 index 0000000..964d420 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala @@ -0,0 +1,21 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) + extends EventStore[Id, T]: + override def persist(id: Id, event: T): Op[Unit] = + data.update { m => + val events = m.getOrElse(id, Nil) + m.updated(id, event :: events) + }.unit + override def get(id: Id): Op[Seq[T]] = + data.get.map(_.getOrElse(id, Nil)) + +object InMemoryEventStore: + def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, List[T]]) + yield InMemoryEventStore(data) + } diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala new file mode 100644 index 0000000..722d7d7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala @@ -0,0 +1,19 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) + extends SnapshotStore[Id, T]: + override def update(id: Id, snapshot: T): UIO[Unit] = + data.update(_.updated(id, snapshot)).unit + + override def get(id: Id): UIO[Option[T]] = + data.get.map(_.get(id)) + +object InMemorySnapshotStore: + def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, T]) + yield InMemorySnapshotStore(data) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala new file mode 100644 index 0000000..874d16c --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala @@ -0,0 +1,63 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import sttp.client3.* + +class ConsulKeyValueStore( + token: ConsulToken, + baseUri: BaseUri, + backend: CustomTapir.Backend +) extends KeyValueStore[String, String]: + + private val defaultBase = "http://localhost:8500" + private val base = baseUri.value.map(_.toString).getOrElse(defaultBase) + + private def addAuth[U[_], T, R]( + request: RequestT[U, T, R] + ): RequestT[U, T, R] = + request.header("X-Consul-Token", token.token) + + override def get(key: String): UIO[Option[String]] = { + for + response <- addAuth(basicRequest.get(uri"$base/v1/kv/${key}?raw=true")) + .send(backend) + yield response.body.toOption + }.orDie + + override def put(key: String, value: String): UIO[Unit] = { + addAuth(basicRequest.put(uri"$base/v1/kv/${key}").body(value)) + .send(backend) + .unit + }.orDie + + override def remove(key: String): UIO[Unit] = { + addAuth(basicRequest.delete(uri"$base/v1/kv/${key}")) + .send(backend) + .unit + }.orDie + +object ConsulKeyValueStore: + val layer: URLayer[ + ConsulToken & BaseUri & CustomTapir.Backend, + StringKeyValueStore + ] = + ZLayer { + for + token <- ZIO.service[ConsulToken] + baseUri <- ZIO.service[BaseUri] + backend <- ZIO.service[CustomTapir.Backend] + yield ConsulKeyValueStore(token, baseUri, backend) + } + + val fromEnv + : ZLayer[CustomTapir.Backend, SecurityException, StringKeyValueStore] = + ConsulToken.fromEnv >>> ZLayer { + for + token <- ZIO.service[ConsulToken] + backend <- ZIO.service[CustomTapir.Backend] + addr <- System.envOrElse("CONSUL_ADDR", "http://localhost:8500") + yield ConsulKeyValueStore(token, BaseUri(addr), backend) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala new file mode 100644 index 0000000..5c5483a --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala @@ -0,0 +1,150 @@ +package works.iterative.core.service.impl + +import zio.* +import sttp.tapir.EndpointIO.annotations.* +import sttp.tapir.Schema +import zio.json.* + +final case class ConsulToken(token: Option[String]) + +object ConsulToken: + def fromEnv: ZLayer[Any, SecurityException, ConsulToken] = ZLayer { + System.env("CONSUL_TOKEN").map(ConsulToken(_)) + } + +@jsonMemberNames(PascalCase) +final case class ConsulMetadata( + createIndex: Long, + modifyIndex: Long, + lockIndex: Long, + key: String, + flags: Long, + value: String, + session: Option[String] +) derives JsonCodec, + Schema + +@endpointInput("v1/kv/{key}") +final case class GetQuery( + @path + @description("Key to get from the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + // TODO: This would not work type-wise properly, as the result type of the call would differ depending on the query parameters + // These are missing, split to different queries to enable. + /* + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies the response is just the raw value of the key, without any encoding or metadata." + ) + raw: Option[Boolean] = None, + @query + @description( + "Specifies to return only keys (no values or metadata). Specifying this parameter implies recurse." + ) + keys: Option[Boolean] = None, + @query + @description( + "Specifies the string to use as a separator for recursive key lookups. This option is only used when paired with the keys parameter to limit the prefix of keys returned, only up to the given separator." + ) + separator: Option[String] = None, + */ + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class PutQuery( + @path + @description("Key to put into the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies an unsigned value between 0 and (2^64)-1 to store with the key. API consumers can use this field any way they choose for their application." + ) + flags: Option[Long] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Supply a session ID to use in a lock acquisition operation.") + acquire: Option[String] = None, + @query + @description("Supply a session ID to use in a release operation.") + release: Option[String] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class DeleteQuery( + @path + @description("Key to delete in the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +/* +sealed trait ConsulGetError +object ConsulGetError: + case object NotFound extends ConsulGetError derives JsonCodec, Schema + case object Forbidden extends ConsulGetError derives JsonCodec, Schema + case class ServerError(msg: String) extends ConsulGetError derives JsonCodec, Schema + +trait ConsulKVEndpoints extends CustomTapir: + private val kvBase = endpoint + .in("v1" / "kv") + .securityIn( + auth.apiKey(header[Option[String]]("X-Consul-Token").mapTo[ConsulToken]) + ) + + val getKV: Endpoint[ConsulToken, GetQuery, Unit, ConsulMetadata, Any] = + kvBase.get + .in(EndpointInput.derived[GetQuery]) + .out(jsonBody[ConsulMetadata]) + .errorOut( + oneOf[ConsulGetError]( + oneOfVariant( + statusCode(StatusCode.NotFound).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfVariant( + statusCode(StatusCode.Forbidden).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfDefaultVariant( + statusCode(StatusCode.InternalServerError).and(jsonBody[ServerError].description("not found")) + ) + ) + ) + val putKV: Endpoint[ConsulToken, PutQuery, Unit, Boolean, Any] = + kvBase.put.in(EndpointInput.derived[PutQuery]).out(jsonBody[Boolean]) + val deleteKV: Endpoint[ConsulToken, DeleteQuery, Unit, Boolean, Any] = + kvBase.delete.in(EndpointInput.derived[DeleteQuery]).out(jsonBody[Boolean]) + +*/ \ No newline at end of file diff --git a/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala new file mode 100644 index 0000000..c938083 --- /dev/null +++ b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala @@ -0,0 +1,19 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import zio.test.* + +object ConsulKeyValueStoreSpec extends specs.KeyValueStoreSpec: + override def spec = defaultSpec.provide( + ConsulKeyValueStore.layer, + CustomTapir.clientLayer, + ZLayer { + Live.live(System.env("CONSUL_ADDR")).map(a => BaseUri(a.get)) + }, + ZLayer { + Live.live(System.env("CONSUL_TOKEN")).map(ConsulToken(_)) + } + ) @@ TestAspect.ifEnvSet("CONSUL_ADDR") diff --git a/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala new file mode 100644 index 0000000..16211a8 --- /dev/null +++ b/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala @@ -0,0 +1,29 @@ +package works.iterative.core.service.impl + +import org.scalajs.dom.Storage +import works.iterative.core.service.Repository +import zio.* +import zio.json.* + +// TODO: improve error reporting on generic repositories +// This is good just for prototypes +// And it cannot be used to query data, as there is no way to iterate the storage +class JsStorageRepository[Value: JsonCodec]( + storage: Storage +) extends Repository[String, Value, String]: + + override def load(id: String): UIO[Option[Value]] = { + for + raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) + data <- ZIO.foreach(raw) { r => + ZIO + .fromEither(r.fromJson[Value]) + .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) + } + yield data + }.orDie + + override def save(key: String, value: Value): UIO[Unit] = + ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie + + override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala new file mode 100644 index 0000000..8c88818 --- /dev/null +++ b/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala @@ -0,0 +1,26 @@ +package works.iterative.core +package service +package impl + +import zio.* +import zio.stream.* + +class JcaDigestService extends DigestService: + + override def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): IO[E, Digest] = + for + md <- ZIO + .attempt( + java.security.MessageDigest.getInstance(algorithm.value) + ) + .orDie + _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) + digest <- ZIO.attempt(md.digest()).orDie + yield Digest(algorithm, digest) + +object JcaDigestGenerator: + val layer: ULayer[DigestService] = + ZLayer.succeed(new JcaDigestService) diff --git a/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala new file mode 100644 index 0000000..883bd7a --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala @@ -0,0 +1,26 @@ +package works.iterative.core.service + +import works.iterative.core.Digest +import works.iterative.core.DigestAlgorithm + +import zio.* +import zio.stream.* + +trait DigestService: + def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): IO[E, Digest] + +object DigestService: + def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): ZIO[DigestService, E, Digest] = + ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) + + def digest( + algorithm: DigestAlgorithm, + content: Array[Byte] + ): URIO[DigestService, Digest] = + digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala new file mode 100644 index 0000000..10bdeb7 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala @@ -0,0 +1,29 @@ +package works.iterative.core.service + +import works.iterative.core.FileSupport.* +import works.iterative.core.FileRef + +import zio.* + +trait FileStore: + type Op[A] = UIO[A] + def store(file: FileRepr): Op[FileRef] + def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] + def load(url: String): Op[Option[Array[Byte]]] + +object FileStore: + type Op[A] = URIO[FileStore, A] + def store(file: FileRepr): Op[FileRef] = + ZIO.serviceWithZIO(_.store(file)) + def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] = + ZIO.serviceWithZIO(_.store(name, file, contentType)) + def load(url: String): Op[Option[Array[Byte]]] = + ZIO.serviceWithZIO(_.load(url)) diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala new file mode 100644 index 0000000..65c7e92 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* + +/** Represents aggregate root in DDD + * + * @tparam Id + * Aggregate root id + * @tparam Command + * Aggregate root command + * @tparam Event + * Aggregate root event + * @tparam State + * Aggregate root state + */ +trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: + /** Aggregate root id */ + def id: Id + + /** Current state */ + def state: UIO[State] + + def handle(command: Command): IO[Error, Unit] diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala new file mode 100644 index 0000000..c051650 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala @@ -0,0 +1,58 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator + +sealed trait FactoryError: + def entityId: String + def userMessage: UserMessage + +object FactoryError: + case class EntityAlreadyExists[Id](entityId: String, id: Id) + extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) + + case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) + +trait AggregateRootFactory[ + Ident, + T <: AggregateRoot[Ident, ?, ?, ?, ?] +]: + type Id = Ident + type NotFound = FactoryError.EntityNotFound[Id] + type AlreadyExists = FactoryError.EntityAlreadyExists[Id] + + protected def AlreadyExists(id: Id): AlreadyExists = + FactoryError.EntityAlreadyExists(entityId, id) + + protected def NotFound(id: Id): NotFound = + FactoryError.EntityNotFound(entityId, id) + + def entityId: String + def make(id: Id): IO[AlreadyExists, T] + def load(id: Id): IO[NotFound, T] + + def loadOrMake(id: Id): UIO[T] = + load(id) + .orElse(make(id)) + .orDieWith(_ => + new RuntimeException( + s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" + ) + ) + + def make()(using idGen: IdGenerator[Id]): UIO[T] = + val tryToCreate = for + id <- idGen.nextId + entity <- make(id) + yield entity + + tryToCreate + .retryN(10) + .orDieWith(_ => + new RuntimeException( + "Cannot create entity, generator failed 10 times to create new ID" + ) + ) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala new file mode 100644 index 0000000..b2fef3a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala @@ -0,0 +1,24 @@ +package works.iterative.entity + +import works.iterative.core.Validated +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator +import zio.* + +/** Generic class for entity factories. + * + * Creates a new entity from a "seed". + */ +trait EntityFactory[Entity, Seed]: + def make(seed: Seed): IO[UserMessage, Entity] + +/** A factory using idGenerator to create the entity */ +class GeneratingFactory[Entity, Seed, Id]( + constructor: Seed => Validated[Id => Entity], + idGenerator: IdGenerator[Id] +) extends EntityFactory[Entity, Seed]: + override def make(seed: Seed): IO[UserMessage, Entity] = + for + construct <- constructor(seed).toZIO + id <- idGenerator.nextId + yield construct(id) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala new file mode 100644 index 0000000..29ae384 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.auth.CurrentUser + +trait EntityUpdateService[Id, Command, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def update(id: Id, command: Command): Op[Unit] + +trait EntityCreateService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(initData: Init): Op[Id] + +trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(id: Id, initData: Init): Op[Id] + +trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] + extends EntityCreateService[Id, Init, Error] + with EntityUpdateService[Id, Command, Error] diff --git a/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala new file mode 100644 index 0000000..946011a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import zio.* + +trait ViewProcessor[Event]: + def process(event: Event): UIO[Unit] diff --git a/entity/shared/src/main/scala/works/iterative/event/Event.scala b/entity/shared/src/main/scala/works/iterative/event/Event.scala new file mode 100644 index 0000000..7907ac0 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/Event.scala @@ -0,0 +1,4 @@ +package works.iterative.event + +trait Event[A]: + def record: EventRecord diff --git a/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala new file mode 100644 index 0000000..490417a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala @@ -0,0 +1,15 @@ +package works.iterative.event + +import works.iterative.core.* +import zio.* + +/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ +final case class EventRecord(userHandle: UserHandle, timestamp: Moment): + val displayName: String = userHandle.displayName + +object EventRecord: + def apply(userHandle: UserHandle): UIO[EventRecord] = + Moment.now.map(EventRecord(userHandle, _)) + + def now(using userHandle: UserHandle): UIO[EventRecord] = + apply(userHandle) diff --git a/entity/shared/src/main/scala/works/iterative/event/EventStore.scala b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala new file mode 100644 index 0000000..3226df3 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala @@ -0,0 +1,13 @@ +package works.iterative.event + +import zio.* + +/** Abstraction of event log + * + * The log can persist events and restore the state of an entity from the + * events. + */ +trait EventStore[Id, T <: Event[_]]: + type Op[A] = UIO[A] + def persist(id: Id, event: T): Op[Unit] + def get(id: Id): Op[Seq[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala new file mode 100644 index 0000000..6ccccd7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala @@ -0,0 +1,12 @@ +package works.iterative.event + +import zio.* + +/** Snapshot store + * + * Stores a snapshot of an entity. + */ +trait SnapshotStore[Id, T]: + type Op[A] = UIO[A] + def update(id: Id, snapshot: T): Op[Unit] + def get(id: Id): Op[Option[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala new file mode 100644 index 0000000..964d420 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala @@ -0,0 +1,21 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) + extends EventStore[Id, T]: + override def persist(id: Id, event: T): Op[Unit] = + data.update { m => + val events = m.getOrElse(id, Nil) + m.updated(id, event :: events) + }.unit + override def get(id: Id): Op[Seq[T]] = + data.get.map(_.getOrElse(id, Nil)) + +object InMemoryEventStore: + def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, List[T]]) + yield InMemoryEventStore(data) + } diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala new file mode 100644 index 0000000..722d7d7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala @@ -0,0 +1,19 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) + extends SnapshotStore[Id, T]: + override def update(id: Id, snapshot: T): UIO[Unit] = + data.update(_.updated(id, snapshot)).unit + + override def get(id: Id): UIO[Option[T]] = + data.get.map(_.get(id)) + +object InMemorySnapshotStore: + def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, T]) + yield InMemorySnapshotStore(data) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala new file mode 100644 index 0000000..874d16c --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala @@ -0,0 +1,63 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import sttp.client3.* + +class ConsulKeyValueStore( + token: ConsulToken, + baseUri: BaseUri, + backend: CustomTapir.Backend +) extends KeyValueStore[String, String]: + + private val defaultBase = "http://localhost:8500" + private val base = baseUri.value.map(_.toString).getOrElse(defaultBase) + + private def addAuth[U[_], T, R]( + request: RequestT[U, T, R] + ): RequestT[U, T, R] = + request.header("X-Consul-Token", token.token) + + override def get(key: String): UIO[Option[String]] = { + for + response <- addAuth(basicRequest.get(uri"$base/v1/kv/${key}?raw=true")) + .send(backend) + yield response.body.toOption + }.orDie + + override def put(key: String, value: String): UIO[Unit] = { + addAuth(basicRequest.put(uri"$base/v1/kv/${key}").body(value)) + .send(backend) + .unit + }.orDie + + override def remove(key: String): UIO[Unit] = { + addAuth(basicRequest.delete(uri"$base/v1/kv/${key}")) + .send(backend) + .unit + }.orDie + +object ConsulKeyValueStore: + val layer: URLayer[ + ConsulToken & BaseUri & CustomTapir.Backend, + StringKeyValueStore + ] = + ZLayer { + for + token <- ZIO.service[ConsulToken] + baseUri <- ZIO.service[BaseUri] + backend <- ZIO.service[CustomTapir.Backend] + yield ConsulKeyValueStore(token, baseUri, backend) + } + + val fromEnv + : ZLayer[CustomTapir.Backend, SecurityException, StringKeyValueStore] = + ConsulToken.fromEnv >>> ZLayer { + for + token <- ZIO.service[ConsulToken] + backend <- ZIO.service[CustomTapir.Backend] + addr <- System.envOrElse("CONSUL_ADDR", "http://localhost:8500") + yield ConsulKeyValueStore(token, BaseUri(addr), backend) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala new file mode 100644 index 0000000..5c5483a --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala @@ -0,0 +1,150 @@ +package works.iterative.core.service.impl + +import zio.* +import sttp.tapir.EndpointIO.annotations.* +import sttp.tapir.Schema +import zio.json.* + +final case class ConsulToken(token: Option[String]) + +object ConsulToken: + def fromEnv: ZLayer[Any, SecurityException, ConsulToken] = ZLayer { + System.env("CONSUL_TOKEN").map(ConsulToken(_)) + } + +@jsonMemberNames(PascalCase) +final case class ConsulMetadata( + createIndex: Long, + modifyIndex: Long, + lockIndex: Long, + key: String, + flags: Long, + value: String, + session: Option[String] +) derives JsonCodec, + Schema + +@endpointInput("v1/kv/{key}") +final case class GetQuery( + @path + @description("Key to get from the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + // TODO: This would not work type-wise properly, as the result type of the call would differ depending on the query parameters + // These are missing, split to different queries to enable. + /* + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies the response is just the raw value of the key, without any encoding or metadata." + ) + raw: Option[Boolean] = None, + @query + @description( + "Specifies to return only keys (no values or metadata). Specifying this parameter implies recurse." + ) + keys: Option[Boolean] = None, + @query + @description( + "Specifies the string to use as a separator for recursive key lookups. This option is only used when paired with the keys parameter to limit the prefix of keys returned, only up to the given separator." + ) + separator: Option[String] = None, + */ + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class PutQuery( + @path + @description("Key to put into the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies an unsigned value between 0 and (2^64)-1 to store with the key. API consumers can use this field any way they choose for their application." + ) + flags: Option[Long] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Supply a session ID to use in a lock acquisition operation.") + acquire: Option[String] = None, + @query + @description("Supply a session ID to use in a release operation.") + release: Option[String] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class DeleteQuery( + @path + @description("Key to delete in the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +/* +sealed trait ConsulGetError +object ConsulGetError: + case object NotFound extends ConsulGetError derives JsonCodec, Schema + case object Forbidden extends ConsulGetError derives JsonCodec, Schema + case class ServerError(msg: String) extends ConsulGetError derives JsonCodec, Schema + +trait ConsulKVEndpoints extends CustomTapir: + private val kvBase = endpoint + .in("v1" / "kv") + .securityIn( + auth.apiKey(header[Option[String]]("X-Consul-Token").mapTo[ConsulToken]) + ) + + val getKV: Endpoint[ConsulToken, GetQuery, Unit, ConsulMetadata, Any] = + kvBase.get + .in(EndpointInput.derived[GetQuery]) + .out(jsonBody[ConsulMetadata]) + .errorOut( + oneOf[ConsulGetError]( + oneOfVariant( + statusCode(StatusCode.NotFound).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfVariant( + statusCode(StatusCode.Forbidden).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfDefaultVariant( + statusCode(StatusCode.InternalServerError).and(jsonBody[ServerError].description("not found")) + ) + ) + ) + val putKV: Endpoint[ConsulToken, PutQuery, Unit, Boolean, Any] = + kvBase.put.in(EndpointInput.derived[PutQuery]).out(jsonBody[Boolean]) + val deleteKV: Endpoint[ConsulToken, DeleteQuery, Unit, Boolean, Any] = + kvBase.delete.in(EndpointInput.derived[DeleteQuery]).out(jsonBody[Boolean]) + +*/ \ No newline at end of file diff --git a/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala new file mode 100644 index 0000000..c938083 --- /dev/null +++ b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala @@ -0,0 +1,19 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import zio.test.* + +object ConsulKeyValueStoreSpec extends specs.KeyValueStoreSpec: + override def spec = defaultSpec.provide( + ConsulKeyValueStore.layer, + CustomTapir.clientLayer, + ZLayer { + Live.live(System.env("CONSUL_ADDR")).map(a => BaseUri(a.get)) + }, + ZLayer { + Live.live(System.env("CONSUL_TOKEN")).map(ConsulToken(_)) + } + ) @@ TestAspect.ifEnvSet("CONSUL_ADDR") diff --git a/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala new file mode 100644 index 0000000..16211a8 --- /dev/null +++ b/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala @@ -0,0 +1,29 @@ +package works.iterative.core.service.impl + +import org.scalajs.dom.Storage +import works.iterative.core.service.Repository +import zio.* +import zio.json.* + +// TODO: improve error reporting on generic repositories +// This is good just for prototypes +// And it cannot be used to query data, as there is no way to iterate the storage +class JsStorageRepository[Value: JsonCodec]( + storage: Storage +) extends Repository[String, Value, String]: + + override def load(id: String): UIO[Option[Value]] = { + for + raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) + data <- ZIO.foreach(raw) { r => + ZIO + .fromEither(r.fromJson[Value]) + .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) + } + yield data + }.orDie + + override def save(key: String, value: Value): UIO[Unit] = + ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie + + override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala new file mode 100644 index 0000000..8c88818 --- /dev/null +++ b/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala @@ -0,0 +1,26 @@ +package works.iterative.core +package service +package impl + +import zio.* +import zio.stream.* + +class JcaDigestService extends DigestService: + + override def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): IO[E, Digest] = + for + md <- ZIO + .attempt( + java.security.MessageDigest.getInstance(algorithm.value) + ) + .orDie + _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) + digest <- ZIO.attempt(md.digest()).orDie + yield Digest(algorithm, digest) + +object JcaDigestGenerator: + val layer: ULayer[DigestService] = + ZLayer.succeed(new JcaDigestService) diff --git a/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala new file mode 100644 index 0000000..883bd7a --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala @@ -0,0 +1,26 @@ +package works.iterative.core.service + +import works.iterative.core.Digest +import works.iterative.core.DigestAlgorithm + +import zio.* +import zio.stream.* + +trait DigestService: + def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): IO[E, Digest] + +object DigestService: + def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): ZIO[DigestService, E, Digest] = + ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) + + def digest( + algorithm: DigestAlgorithm, + content: Array[Byte] + ): URIO[DigestService, Digest] = + digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala new file mode 100644 index 0000000..10bdeb7 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala @@ -0,0 +1,29 @@ +package works.iterative.core.service + +import works.iterative.core.FileSupport.* +import works.iterative.core.FileRef + +import zio.* + +trait FileStore: + type Op[A] = UIO[A] + def store(file: FileRepr): Op[FileRef] + def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] + def load(url: String): Op[Option[Array[Byte]]] + +object FileStore: + type Op[A] = URIO[FileStore, A] + def store(file: FileRepr): Op[FileRef] = + ZIO.serviceWithZIO(_.store(file)) + def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] = + ZIO.serviceWithZIO(_.store(name, file, contentType)) + def load(url: String): Op[Option[Array[Byte]]] = + ZIO.serviceWithZIO(_.load(url)) diff --git a/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala new file mode 100644 index 0000000..1779da3 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala @@ -0,0 +1,10 @@ +package works.iterative.core.service + +import zio.* + +/** Generator of unique IDs of a given type */ +trait IdGenerator[A]: + self => + def nextId: UIO[A] + def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: + def nextId: UIO[B] = self.nextId.map(f) diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala new file mode 100644 index 0000000..65c7e92 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* + +/** Represents aggregate root in DDD + * + * @tparam Id + * Aggregate root id + * @tparam Command + * Aggregate root command + * @tparam Event + * Aggregate root event + * @tparam State + * Aggregate root state + */ +trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: + /** Aggregate root id */ + def id: Id + + /** Current state */ + def state: UIO[State] + + def handle(command: Command): IO[Error, Unit] diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala new file mode 100644 index 0000000..c051650 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala @@ -0,0 +1,58 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator + +sealed trait FactoryError: + def entityId: String + def userMessage: UserMessage + +object FactoryError: + case class EntityAlreadyExists[Id](entityId: String, id: Id) + extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) + + case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) + +trait AggregateRootFactory[ + Ident, + T <: AggregateRoot[Ident, ?, ?, ?, ?] +]: + type Id = Ident + type NotFound = FactoryError.EntityNotFound[Id] + type AlreadyExists = FactoryError.EntityAlreadyExists[Id] + + protected def AlreadyExists(id: Id): AlreadyExists = + FactoryError.EntityAlreadyExists(entityId, id) + + protected def NotFound(id: Id): NotFound = + FactoryError.EntityNotFound(entityId, id) + + def entityId: String + def make(id: Id): IO[AlreadyExists, T] + def load(id: Id): IO[NotFound, T] + + def loadOrMake(id: Id): UIO[T] = + load(id) + .orElse(make(id)) + .orDieWith(_ => + new RuntimeException( + s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" + ) + ) + + def make()(using idGen: IdGenerator[Id]): UIO[T] = + val tryToCreate = for + id <- idGen.nextId + entity <- make(id) + yield entity + + tryToCreate + .retryN(10) + .orDieWith(_ => + new RuntimeException( + "Cannot create entity, generator failed 10 times to create new ID" + ) + ) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala new file mode 100644 index 0000000..b2fef3a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala @@ -0,0 +1,24 @@ +package works.iterative.entity + +import works.iterative.core.Validated +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator +import zio.* + +/** Generic class for entity factories. + * + * Creates a new entity from a "seed". + */ +trait EntityFactory[Entity, Seed]: + def make(seed: Seed): IO[UserMessage, Entity] + +/** A factory using idGenerator to create the entity */ +class GeneratingFactory[Entity, Seed, Id]( + constructor: Seed => Validated[Id => Entity], + idGenerator: IdGenerator[Id] +) extends EntityFactory[Entity, Seed]: + override def make(seed: Seed): IO[UserMessage, Entity] = + for + construct <- constructor(seed).toZIO + id <- idGenerator.nextId + yield construct(id) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala new file mode 100644 index 0000000..29ae384 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.auth.CurrentUser + +trait EntityUpdateService[Id, Command, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def update(id: Id, command: Command): Op[Unit] + +trait EntityCreateService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(initData: Init): Op[Id] + +trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(id: Id, initData: Init): Op[Id] + +trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] + extends EntityCreateService[Id, Init, Error] + with EntityUpdateService[Id, Command, Error] diff --git a/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala new file mode 100644 index 0000000..946011a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import zio.* + +trait ViewProcessor[Event]: + def process(event: Event): UIO[Unit] diff --git a/entity/shared/src/main/scala/works/iterative/event/Event.scala b/entity/shared/src/main/scala/works/iterative/event/Event.scala new file mode 100644 index 0000000..7907ac0 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/Event.scala @@ -0,0 +1,4 @@ +package works.iterative.event + +trait Event[A]: + def record: EventRecord diff --git a/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala new file mode 100644 index 0000000..490417a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala @@ -0,0 +1,15 @@ +package works.iterative.event + +import works.iterative.core.* +import zio.* + +/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ +final case class EventRecord(userHandle: UserHandle, timestamp: Moment): + val displayName: String = userHandle.displayName + +object EventRecord: + def apply(userHandle: UserHandle): UIO[EventRecord] = + Moment.now.map(EventRecord(userHandle, _)) + + def now(using userHandle: UserHandle): UIO[EventRecord] = + apply(userHandle) diff --git a/entity/shared/src/main/scala/works/iterative/event/EventStore.scala b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala new file mode 100644 index 0000000..3226df3 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala @@ -0,0 +1,13 @@ +package works.iterative.event + +import zio.* + +/** Abstraction of event log + * + * The log can persist events and restore the state of an entity from the + * events. + */ +trait EventStore[Id, T <: Event[_]]: + type Op[A] = UIO[A] + def persist(id: Id, event: T): Op[Unit] + def get(id: Id): Op[Seq[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala new file mode 100644 index 0000000..6ccccd7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala @@ -0,0 +1,12 @@ +package works.iterative.event + +import zio.* + +/** Snapshot store + * + * Stores a snapshot of an entity. + */ +trait SnapshotStore[Id, T]: + type Op[A] = UIO[A] + def update(id: Id, snapshot: T): Op[Unit] + def get(id: Id): Op[Option[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala new file mode 100644 index 0000000..964d420 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala @@ -0,0 +1,21 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) + extends EventStore[Id, T]: + override def persist(id: Id, event: T): Op[Unit] = + data.update { m => + val events = m.getOrElse(id, Nil) + m.updated(id, event :: events) + }.unit + override def get(id: Id): Op[Seq[T]] = + data.get.map(_.getOrElse(id, Nil)) + +object InMemoryEventStore: + def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, List[T]]) + yield InMemoryEventStore(data) + } diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala new file mode 100644 index 0000000..722d7d7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala @@ -0,0 +1,19 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) + extends SnapshotStore[Id, T]: + override def update(id: Id, snapshot: T): UIO[Unit] = + data.update(_.updated(id, snapshot)).unit + + override def get(id: Id): UIO[Option[T]] = + data.get.map(_.get(id)) + +object InMemorySnapshotStore: + def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, T]) + yield InMemorySnapshotStore(data) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala new file mode 100644 index 0000000..874d16c --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala @@ -0,0 +1,63 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import sttp.client3.* + +class ConsulKeyValueStore( + token: ConsulToken, + baseUri: BaseUri, + backend: CustomTapir.Backend +) extends KeyValueStore[String, String]: + + private val defaultBase = "http://localhost:8500" + private val base = baseUri.value.map(_.toString).getOrElse(defaultBase) + + private def addAuth[U[_], T, R]( + request: RequestT[U, T, R] + ): RequestT[U, T, R] = + request.header("X-Consul-Token", token.token) + + override def get(key: String): UIO[Option[String]] = { + for + response <- addAuth(basicRequest.get(uri"$base/v1/kv/${key}?raw=true")) + .send(backend) + yield response.body.toOption + }.orDie + + override def put(key: String, value: String): UIO[Unit] = { + addAuth(basicRequest.put(uri"$base/v1/kv/${key}").body(value)) + .send(backend) + .unit + }.orDie + + override def remove(key: String): UIO[Unit] = { + addAuth(basicRequest.delete(uri"$base/v1/kv/${key}")) + .send(backend) + .unit + }.orDie + +object ConsulKeyValueStore: + val layer: URLayer[ + ConsulToken & BaseUri & CustomTapir.Backend, + StringKeyValueStore + ] = + ZLayer { + for + token <- ZIO.service[ConsulToken] + baseUri <- ZIO.service[BaseUri] + backend <- ZIO.service[CustomTapir.Backend] + yield ConsulKeyValueStore(token, baseUri, backend) + } + + val fromEnv + : ZLayer[CustomTapir.Backend, SecurityException, StringKeyValueStore] = + ConsulToken.fromEnv >>> ZLayer { + for + token <- ZIO.service[ConsulToken] + backend <- ZIO.service[CustomTapir.Backend] + addr <- System.envOrElse("CONSUL_ADDR", "http://localhost:8500") + yield ConsulKeyValueStore(token, BaseUri(addr), backend) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala new file mode 100644 index 0000000..5c5483a --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala @@ -0,0 +1,150 @@ +package works.iterative.core.service.impl + +import zio.* +import sttp.tapir.EndpointIO.annotations.* +import sttp.tapir.Schema +import zio.json.* + +final case class ConsulToken(token: Option[String]) + +object ConsulToken: + def fromEnv: ZLayer[Any, SecurityException, ConsulToken] = ZLayer { + System.env("CONSUL_TOKEN").map(ConsulToken(_)) + } + +@jsonMemberNames(PascalCase) +final case class ConsulMetadata( + createIndex: Long, + modifyIndex: Long, + lockIndex: Long, + key: String, + flags: Long, + value: String, + session: Option[String] +) derives JsonCodec, + Schema + +@endpointInput("v1/kv/{key}") +final case class GetQuery( + @path + @description("Key to get from the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + // TODO: This would not work type-wise properly, as the result type of the call would differ depending on the query parameters + // These are missing, split to different queries to enable. + /* + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies the response is just the raw value of the key, without any encoding or metadata." + ) + raw: Option[Boolean] = None, + @query + @description( + "Specifies to return only keys (no values or metadata). Specifying this parameter implies recurse." + ) + keys: Option[Boolean] = None, + @query + @description( + "Specifies the string to use as a separator for recursive key lookups. This option is only used when paired with the keys parameter to limit the prefix of keys returned, only up to the given separator." + ) + separator: Option[String] = None, + */ + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class PutQuery( + @path + @description("Key to put into the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies an unsigned value between 0 and (2^64)-1 to store with the key. API consumers can use this field any way they choose for their application." + ) + flags: Option[Long] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Supply a session ID to use in a lock acquisition operation.") + acquire: Option[String] = None, + @query + @description("Supply a session ID to use in a release operation.") + release: Option[String] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class DeleteQuery( + @path + @description("Key to delete in the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +/* +sealed trait ConsulGetError +object ConsulGetError: + case object NotFound extends ConsulGetError derives JsonCodec, Schema + case object Forbidden extends ConsulGetError derives JsonCodec, Schema + case class ServerError(msg: String) extends ConsulGetError derives JsonCodec, Schema + +trait ConsulKVEndpoints extends CustomTapir: + private val kvBase = endpoint + .in("v1" / "kv") + .securityIn( + auth.apiKey(header[Option[String]]("X-Consul-Token").mapTo[ConsulToken]) + ) + + val getKV: Endpoint[ConsulToken, GetQuery, Unit, ConsulMetadata, Any] = + kvBase.get + .in(EndpointInput.derived[GetQuery]) + .out(jsonBody[ConsulMetadata]) + .errorOut( + oneOf[ConsulGetError]( + oneOfVariant( + statusCode(StatusCode.NotFound).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfVariant( + statusCode(StatusCode.Forbidden).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfDefaultVariant( + statusCode(StatusCode.InternalServerError).and(jsonBody[ServerError].description("not found")) + ) + ) + ) + val putKV: Endpoint[ConsulToken, PutQuery, Unit, Boolean, Any] = + kvBase.put.in(EndpointInput.derived[PutQuery]).out(jsonBody[Boolean]) + val deleteKV: Endpoint[ConsulToken, DeleteQuery, Unit, Boolean, Any] = + kvBase.delete.in(EndpointInput.derived[DeleteQuery]).out(jsonBody[Boolean]) + +*/ \ No newline at end of file diff --git a/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala new file mode 100644 index 0000000..c938083 --- /dev/null +++ b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala @@ -0,0 +1,19 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import zio.test.* + +object ConsulKeyValueStoreSpec extends specs.KeyValueStoreSpec: + override def spec = defaultSpec.provide( + ConsulKeyValueStore.layer, + CustomTapir.clientLayer, + ZLayer { + Live.live(System.env("CONSUL_ADDR")).map(a => BaseUri(a.get)) + }, + ZLayer { + Live.live(System.env("CONSUL_TOKEN")).map(ConsulToken(_)) + } + ) @@ TestAspect.ifEnvSet("CONSUL_ADDR") diff --git a/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala new file mode 100644 index 0000000..16211a8 --- /dev/null +++ b/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala @@ -0,0 +1,29 @@ +package works.iterative.core.service.impl + +import org.scalajs.dom.Storage +import works.iterative.core.service.Repository +import zio.* +import zio.json.* + +// TODO: improve error reporting on generic repositories +// This is good just for prototypes +// And it cannot be used to query data, as there is no way to iterate the storage +class JsStorageRepository[Value: JsonCodec]( + storage: Storage +) extends Repository[String, Value, String]: + + override def load(id: String): UIO[Option[Value]] = { + for + raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) + data <- ZIO.foreach(raw) { r => + ZIO + .fromEither(r.fromJson[Value]) + .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) + } + yield data + }.orDie + + override def save(key: String, value: Value): UIO[Unit] = + ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie + + override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala new file mode 100644 index 0000000..8c88818 --- /dev/null +++ b/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala @@ -0,0 +1,26 @@ +package works.iterative.core +package service +package impl + +import zio.* +import zio.stream.* + +class JcaDigestService extends DigestService: + + override def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): IO[E, Digest] = + for + md <- ZIO + .attempt( + java.security.MessageDigest.getInstance(algorithm.value) + ) + .orDie + _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) + digest <- ZIO.attempt(md.digest()).orDie + yield Digest(algorithm, digest) + +object JcaDigestGenerator: + val layer: ULayer[DigestService] = + ZLayer.succeed(new JcaDigestService) diff --git a/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala new file mode 100644 index 0000000..883bd7a --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala @@ -0,0 +1,26 @@ +package works.iterative.core.service + +import works.iterative.core.Digest +import works.iterative.core.DigestAlgorithm + +import zio.* +import zio.stream.* + +trait DigestService: + def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): IO[E, Digest] + +object DigestService: + def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): ZIO[DigestService, E, Digest] = + ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) + + def digest( + algorithm: DigestAlgorithm, + content: Array[Byte] + ): URIO[DigestService, Digest] = + digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala new file mode 100644 index 0000000..10bdeb7 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala @@ -0,0 +1,29 @@ +package works.iterative.core.service + +import works.iterative.core.FileSupport.* +import works.iterative.core.FileRef + +import zio.* + +trait FileStore: + type Op[A] = UIO[A] + def store(file: FileRepr): Op[FileRef] + def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] + def load(url: String): Op[Option[Array[Byte]]] + +object FileStore: + type Op[A] = URIO[FileStore, A] + def store(file: FileRepr): Op[FileRef] = + ZIO.serviceWithZIO(_.store(file)) + def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] = + ZIO.serviceWithZIO(_.store(name, file, contentType)) + def load(url: String): Op[Option[Array[Byte]]] = + ZIO.serviceWithZIO(_.load(url)) diff --git a/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala new file mode 100644 index 0000000..1779da3 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala @@ -0,0 +1,10 @@ +package works.iterative.core.service + +import zio.* + +/** Generator of unique IDs of a given type */ +trait IdGenerator[A]: + self => + def nextId: UIO[A] + def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: + def nextId: UIO[B] = self.nextId.map(f) diff --git a/service/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/service/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala new file mode 100644 index 0000000..23d1908 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala @@ -0,0 +1,47 @@ +package works.iterative.core.service + +import zio.* +import zio.json.* + +trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: + type Op[A] = Eff[A] + def get(key: Key): Eff[Option[Value]] + +trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: + def put(key: Key, value: Value): Eff[Unit] + def remove(key: Key): Eff[Unit] + +trait GenericKeyValueStore[Eff[+_], -Key, Value] + extends GenericReadKeyValueStore[Eff, Key, Value] + with GenericWriteKeyValueStore[Eff, Key, Value] + +trait ReadKeyValueStore[-Key, +Value] + extends GenericReadKeyValueStore[UIO, Key, Value] + +trait WriteKeyValueStore[-Key, -Value] + extends GenericWriteKeyValueStore[UIO, Key, Value] + +trait KeyValueStore[-Key, Value] + extends GenericKeyValueStore[UIO, Key, Value] + with ReadKeyValueStore[Key, Value] + with WriteKeyValueStore[Key, Value] + +type StringKeyValueStore = KeyValueStore[String, String] + +object KeyValueStore: + extension (store: StringKeyValueStore) + /** Decode the value, ignoring decoding errors if any */ + def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = + store.get(key).map(_.flatMap(_.fromJson[A].toOption)) + + def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = + store.get(key).flatMap { + case None => ZIO.none + case Some(v) => + ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => + ZIO.log(err) *> ZIO.none + } + } + + def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = + store.put(key, value.toJson) diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala new file mode 100644 index 0000000..65c7e92 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* + +/** Represents aggregate root in DDD + * + * @tparam Id + * Aggregate root id + * @tparam Command + * Aggregate root command + * @tparam Event + * Aggregate root event + * @tparam State + * Aggregate root state + */ +trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: + /** Aggregate root id */ + def id: Id + + /** Current state */ + def state: UIO[State] + + def handle(command: Command): IO[Error, Unit] diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala new file mode 100644 index 0000000..c051650 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala @@ -0,0 +1,58 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator + +sealed trait FactoryError: + def entityId: String + def userMessage: UserMessage + +object FactoryError: + case class EntityAlreadyExists[Id](entityId: String, id: Id) + extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) + + case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) + +trait AggregateRootFactory[ + Ident, + T <: AggregateRoot[Ident, ?, ?, ?, ?] +]: + type Id = Ident + type NotFound = FactoryError.EntityNotFound[Id] + type AlreadyExists = FactoryError.EntityAlreadyExists[Id] + + protected def AlreadyExists(id: Id): AlreadyExists = + FactoryError.EntityAlreadyExists(entityId, id) + + protected def NotFound(id: Id): NotFound = + FactoryError.EntityNotFound(entityId, id) + + def entityId: String + def make(id: Id): IO[AlreadyExists, T] + def load(id: Id): IO[NotFound, T] + + def loadOrMake(id: Id): UIO[T] = + load(id) + .orElse(make(id)) + .orDieWith(_ => + new RuntimeException( + s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" + ) + ) + + def make()(using idGen: IdGenerator[Id]): UIO[T] = + val tryToCreate = for + id <- idGen.nextId + entity <- make(id) + yield entity + + tryToCreate + .retryN(10) + .orDieWith(_ => + new RuntimeException( + "Cannot create entity, generator failed 10 times to create new ID" + ) + ) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala new file mode 100644 index 0000000..b2fef3a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala @@ -0,0 +1,24 @@ +package works.iterative.entity + +import works.iterative.core.Validated +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator +import zio.* + +/** Generic class for entity factories. + * + * Creates a new entity from a "seed". + */ +trait EntityFactory[Entity, Seed]: + def make(seed: Seed): IO[UserMessage, Entity] + +/** A factory using idGenerator to create the entity */ +class GeneratingFactory[Entity, Seed, Id]( + constructor: Seed => Validated[Id => Entity], + idGenerator: IdGenerator[Id] +) extends EntityFactory[Entity, Seed]: + override def make(seed: Seed): IO[UserMessage, Entity] = + for + construct <- constructor(seed).toZIO + id <- idGenerator.nextId + yield construct(id) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala new file mode 100644 index 0000000..29ae384 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.auth.CurrentUser + +trait EntityUpdateService[Id, Command, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def update(id: Id, command: Command): Op[Unit] + +trait EntityCreateService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(initData: Init): Op[Id] + +trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(id: Id, initData: Init): Op[Id] + +trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] + extends EntityCreateService[Id, Init, Error] + with EntityUpdateService[Id, Command, Error] diff --git a/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala new file mode 100644 index 0000000..946011a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import zio.* + +trait ViewProcessor[Event]: + def process(event: Event): UIO[Unit] diff --git a/entity/shared/src/main/scala/works/iterative/event/Event.scala b/entity/shared/src/main/scala/works/iterative/event/Event.scala new file mode 100644 index 0000000..7907ac0 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/Event.scala @@ -0,0 +1,4 @@ +package works.iterative.event + +trait Event[A]: + def record: EventRecord diff --git a/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala new file mode 100644 index 0000000..490417a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala @@ -0,0 +1,15 @@ +package works.iterative.event + +import works.iterative.core.* +import zio.* + +/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ +final case class EventRecord(userHandle: UserHandle, timestamp: Moment): + val displayName: String = userHandle.displayName + +object EventRecord: + def apply(userHandle: UserHandle): UIO[EventRecord] = + Moment.now.map(EventRecord(userHandle, _)) + + def now(using userHandle: UserHandle): UIO[EventRecord] = + apply(userHandle) diff --git a/entity/shared/src/main/scala/works/iterative/event/EventStore.scala b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala new file mode 100644 index 0000000..3226df3 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala @@ -0,0 +1,13 @@ +package works.iterative.event + +import zio.* + +/** Abstraction of event log + * + * The log can persist events and restore the state of an entity from the + * events. + */ +trait EventStore[Id, T <: Event[_]]: + type Op[A] = UIO[A] + def persist(id: Id, event: T): Op[Unit] + def get(id: Id): Op[Seq[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala new file mode 100644 index 0000000..6ccccd7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala @@ -0,0 +1,12 @@ +package works.iterative.event + +import zio.* + +/** Snapshot store + * + * Stores a snapshot of an entity. + */ +trait SnapshotStore[Id, T]: + type Op[A] = UIO[A] + def update(id: Id, snapshot: T): Op[Unit] + def get(id: Id): Op[Option[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala new file mode 100644 index 0000000..964d420 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala @@ -0,0 +1,21 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) + extends EventStore[Id, T]: + override def persist(id: Id, event: T): Op[Unit] = + data.update { m => + val events = m.getOrElse(id, Nil) + m.updated(id, event :: events) + }.unit + override def get(id: Id): Op[Seq[T]] = + data.get.map(_.getOrElse(id, Nil)) + +object InMemoryEventStore: + def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, List[T]]) + yield InMemoryEventStore(data) + } diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala new file mode 100644 index 0000000..722d7d7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala @@ -0,0 +1,19 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) + extends SnapshotStore[Id, T]: + override def update(id: Id, snapshot: T): UIO[Unit] = + data.update(_.updated(id, snapshot)).unit + + override def get(id: Id): UIO[Option[T]] = + data.get.map(_.get(id)) + +object InMemorySnapshotStore: + def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, T]) + yield InMemorySnapshotStore(data) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala new file mode 100644 index 0000000..874d16c --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala @@ -0,0 +1,63 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import sttp.client3.* + +class ConsulKeyValueStore( + token: ConsulToken, + baseUri: BaseUri, + backend: CustomTapir.Backend +) extends KeyValueStore[String, String]: + + private val defaultBase = "http://localhost:8500" + private val base = baseUri.value.map(_.toString).getOrElse(defaultBase) + + private def addAuth[U[_], T, R]( + request: RequestT[U, T, R] + ): RequestT[U, T, R] = + request.header("X-Consul-Token", token.token) + + override def get(key: String): UIO[Option[String]] = { + for + response <- addAuth(basicRequest.get(uri"$base/v1/kv/${key}?raw=true")) + .send(backend) + yield response.body.toOption + }.orDie + + override def put(key: String, value: String): UIO[Unit] = { + addAuth(basicRequest.put(uri"$base/v1/kv/${key}").body(value)) + .send(backend) + .unit + }.orDie + + override def remove(key: String): UIO[Unit] = { + addAuth(basicRequest.delete(uri"$base/v1/kv/${key}")) + .send(backend) + .unit + }.orDie + +object ConsulKeyValueStore: + val layer: URLayer[ + ConsulToken & BaseUri & CustomTapir.Backend, + StringKeyValueStore + ] = + ZLayer { + for + token <- ZIO.service[ConsulToken] + baseUri <- ZIO.service[BaseUri] + backend <- ZIO.service[CustomTapir.Backend] + yield ConsulKeyValueStore(token, baseUri, backend) + } + + val fromEnv + : ZLayer[CustomTapir.Backend, SecurityException, StringKeyValueStore] = + ConsulToken.fromEnv >>> ZLayer { + for + token <- ZIO.service[ConsulToken] + backend <- ZIO.service[CustomTapir.Backend] + addr <- System.envOrElse("CONSUL_ADDR", "http://localhost:8500") + yield ConsulKeyValueStore(token, BaseUri(addr), backend) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala new file mode 100644 index 0000000..5c5483a --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala @@ -0,0 +1,150 @@ +package works.iterative.core.service.impl + +import zio.* +import sttp.tapir.EndpointIO.annotations.* +import sttp.tapir.Schema +import zio.json.* + +final case class ConsulToken(token: Option[String]) + +object ConsulToken: + def fromEnv: ZLayer[Any, SecurityException, ConsulToken] = ZLayer { + System.env("CONSUL_TOKEN").map(ConsulToken(_)) + } + +@jsonMemberNames(PascalCase) +final case class ConsulMetadata( + createIndex: Long, + modifyIndex: Long, + lockIndex: Long, + key: String, + flags: Long, + value: String, + session: Option[String] +) derives JsonCodec, + Schema + +@endpointInput("v1/kv/{key}") +final case class GetQuery( + @path + @description("Key to get from the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + // TODO: This would not work type-wise properly, as the result type of the call would differ depending on the query parameters + // These are missing, split to different queries to enable. + /* + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies the response is just the raw value of the key, without any encoding or metadata." + ) + raw: Option[Boolean] = None, + @query + @description( + "Specifies to return only keys (no values or metadata). Specifying this parameter implies recurse." + ) + keys: Option[Boolean] = None, + @query + @description( + "Specifies the string to use as a separator for recursive key lookups. This option is only used when paired with the keys parameter to limit the prefix of keys returned, only up to the given separator." + ) + separator: Option[String] = None, + */ + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class PutQuery( + @path + @description("Key to put into the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies an unsigned value between 0 and (2^64)-1 to store with the key. API consumers can use this field any way they choose for their application." + ) + flags: Option[Long] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Supply a session ID to use in a lock acquisition operation.") + acquire: Option[String] = None, + @query + @description("Supply a session ID to use in a release operation.") + release: Option[String] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class DeleteQuery( + @path + @description("Key to delete in the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +/* +sealed trait ConsulGetError +object ConsulGetError: + case object NotFound extends ConsulGetError derives JsonCodec, Schema + case object Forbidden extends ConsulGetError derives JsonCodec, Schema + case class ServerError(msg: String) extends ConsulGetError derives JsonCodec, Schema + +trait ConsulKVEndpoints extends CustomTapir: + private val kvBase = endpoint + .in("v1" / "kv") + .securityIn( + auth.apiKey(header[Option[String]]("X-Consul-Token").mapTo[ConsulToken]) + ) + + val getKV: Endpoint[ConsulToken, GetQuery, Unit, ConsulMetadata, Any] = + kvBase.get + .in(EndpointInput.derived[GetQuery]) + .out(jsonBody[ConsulMetadata]) + .errorOut( + oneOf[ConsulGetError]( + oneOfVariant( + statusCode(StatusCode.NotFound).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfVariant( + statusCode(StatusCode.Forbidden).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfDefaultVariant( + statusCode(StatusCode.InternalServerError).and(jsonBody[ServerError].description("not found")) + ) + ) + ) + val putKV: Endpoint[ConsulToken, PutQuery, Unit, Boolean, Any] = + kvBase.put.in(EndpointInput.derived[PutQuery]).out(jsonBody[Boolean]) + val deleteKV: Endpoint[ConsulToken, DeleteQuery, Unit, Boolean, Any] = + kvBase.delete.in(EndpointInput.derived[DeleteQuery]).out(jsonBody[Boolean]) + +*/ \ No newline at end of file diff --git a/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala new file mode 100644 index 0000000..c938083 --- /dev/null +++ b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala @@ -0,0 +1,19 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import zio.test.* + +object ConsulKeyValueStoreSpec extends specs.KeyValueStoreSpec: + override def spec = defaultSpec.provide( + ConsulKeyValueStore.layer, + CustomTapir.clientLayer, + ZLayer { + Live.live(System.env("CONSUL_ADDR")).map(a => BaseUri(a.get)) + }, + ZLayer { + Live.live(System.env("CONSUL_TOKEN")).map(ConsulToken(_)) + } + ) @@ TestAspect.ifEnvSet("CONSUL_ADDR") diff --git a/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala new file mode 100644 index 0000000..16211a8 --- /dev/null +++ b/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala @@ -0,0 +1,29 @@ +package works.iterative.core.service.impl + +import org.scalajs.dom.Storage +import works.iterative.core.service.Repository +import zio.* +import zio.json.* + +// TODO: improve error reporting on generic repositories +// This is good just for prototypes +// And it cannot be used to query data, as there is no way to iterate the storage +class JsStorageRepository[Value: JsonCodec]( + storage: Storage +) extends Repository[String, Value, String]: + + override def load(id: String): UIO[Option[Value]] = { + for + raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) + data <- ZIO.foreach(raw) { r => + ZIO + .fromEither(r.fromJson[Value]) + .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) + } + yield data + }.orDie + + override def save(key: String, value: Value): UIO[Unit] = + ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie + + override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala new file mode 100644 index 0000000..8c88818 --- /dev/null +++ b/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala @@ -0,0 +1,26 @@ +package works.iterative.core +package service +package impl + +import zio.* +import zio.stream.* + +class JcaDigestService extends DigestService: + + override def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): IO[E, Digest] = + for + md <- ZIO + .attempt( + java.security.MessageDigest.getInstance(algorithm.value) + ) + .orDie + _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) + digest <- ZIO.attempt(md.digest()).orDie + yield Digest(algorithm, digest) + +object JcaDigestGenerator: + val layer: ULayer[DigestService] = + ZLayer.succeed(new JcaDigestService) diff --git a/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala new file mode 100644 index 0000000..883bd7a --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala @@ -0,0 +1,26 @@ +package works.iterative.core.service + +import works.iterative.core.Digest +import works.iterative.core.DigestAlgorithm + +import zio.* +import zio.stream.* + +trait DigestService: + def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): IO[E, Digest] + +object DigestService: + def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): ZIO[DigestService, E, Digest] = + ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) + + def digest( + algorithm: DigestAlgorithm, + content: Array[Byte] + ): URIO[DigestService, Digest] = + digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala new file mode 100644 index 0000000..10bdeb7 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala @@ -0,0 +1,29 @@ +package works.iterative.core.service + +import works.iterative.core.FileSupport.* +import works.iterative.core.FileRef + +import zio.* + +trait FileStore: + type Op[A] = UIO[A] + def store(file: FileRepr): Op[FileRef] + def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] + def load(url: String): Op[Option[Array[Byte]]] + +object FileStore: + type Op[A] = URIO[FileStore, A] + def store(file: FileRepr): Op[FileRef] = + ZIO.serviceWithZIO(_.store(file)) + def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] = + ZIO.serviceWithZIO(_.store(name, file, contentType)) + def load(url: String): Op[Option[Array[Byte]]] = + ZIO.serviceWithZIO(_.load(url)) diff --git a/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala new file mode 100644 index 0000000..1779da3 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala @@ -0,0 +1,10 @@ +package works.iterative.core.service + +import zio.* + +/** Generator of unique IDs of a given type */ +trait IdGenerator[A]: + self => + def nextId: UIO[A] + def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: + def nextId: UIO[B] = self.nextId.map(f) diff --git a/service/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/service/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala new file mode 100644 index 0000000..23d1908 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala @@ -0,0 +1,47 @@ +package works.iterative.core.service + +import zio.* +import zio.json.* + +trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: + type Op[A] = Eff[A] + def get(key: Key): Eff[Option[Value]] + +trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: + def put(key: Key, value: Value): Eff[Unit] + def remove(key: Key): Eff[Unit] + +trait GenericKeyValueStore[Eff[+_], -Key, Value] + extends GenericReadKeyValueStore[Eff, Key, Value] + with GenericWriteKeyValueStore[Eff, Key, Value] + +trait ReadKeyValueStore[-Key, +Value] + extends GenericReadKeyValueStore[UIO, Key, Value] + +trait WriteKeyValueStore[-Key, -Value] + extends GenericWriteKeyValueStore[UIO, Key, Value] + +trait KeyValueStore[-Key, Value] + extends GenericKeyValueStore[UIO, Key, Value] + with ReadKeyValueStore[Key, Value] + with WriteKeyValueStore[Key, Value] + +type StringKeyValueStore = KeyValueStore[String, String] + +object KeyValueStore: + extension (store: StringKeyValueStore) + /** Decode the value, ignoring decoding errors if any */ + def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = + store.get(key).map(_.flatMap(_.fromJson[A].toOption)) + + def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = + store.get(key).flatMap { + case None => ZIO.none + case Some(v) => + ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => + ZIO.log(err) *> ZIO.none + } + } + + def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = + store.put(key, value.toJson) diff --git a/service/shared/src/main/scala/works/iterative/core/service/Repository.scala b/service/shared/src/main/scala/works/iterative/core/service/Repository.scala new file mode 100644 index 0000000..c450279 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/Repository.scala @@ -0,0 +1,52 @@ +package works.iterative.core.service + +import zio.* +import zio.stream.* + +trait GenericLoadService[Eff[+_], -Key, +Value]: + type Op[A] = Eff[A] + def load(id: Key): Op[Option[Value]] + +trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: + def updates: Eff[Str[Key]] + +trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: + type Op[A] = Eff[A] + def loadAll(ids: Seq[Key]): Op[Coll[Value]] + +trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: + type Op[A] = Eff[A] + def find(filter: FilterArg): Op[Coll[Value]] + +trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] + extends GenericLoadService[Eff, Key, Value] + with GenericLoadAllService[Eff, Coll, Key, Value] + with GenericFindService[Eff, Coll, Key, Value, FilterArg] + +trait GenericWriteRepository[Eff[_], -Key, -Value]: + type Op[A] = Eff[A] + def save(key: Key, value: Value): Op[Unit] + +trait GenericRepository[Eff[+_], -Key, Value] + extends GenericReadRepository[Eff, List, Key, Value, Unit] + with GenericWriteRepository[Eff, Key, Value] + +type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] +type LoadAllRepository[-Key, +Value] = + GenericLoadAllService[UIO, List, Key, Value] + +trait ReadRepository[-Key, +Value, -FilterArg] + extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: + override def loadAll(ids: Seq[Key]): UIO[List[Value]] = + // Inefficient implementation, meant to be overridden + ZIO.foreach(ids)(load).map(_.flatten.toList) + +trait UpdateNotifyRepository[Key] + extends GenericUpdateNotifyService[UIO, UStream, Key] + +trait WriteRepository[-Key, -Value] + extends GenericWriteRepository[UIO, Key, Value] + +trait Repository[-Key, Value, -FilterArg] + extends ReadRepository[Key, Value, FilterArg] + with WriteRepository[Key, Value] diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala new file mode 100644 index 0000000..65c7e92 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* + +/** Represents aggregate root in DDD + * + * @tparam Id + * Aggregate root id + * @tparam Command + * Aggregate root command + * @tparam Event + * Aggregate root event + * @tparam State + * Aggregate root state + */ +trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: + /** Aggregate root id */ + def id: Id + + /** Current state */ + def state: UIO[State] + + def handle(command: Command): IO[Error, Unit] diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala new file mode 100644 index 0000000..c051650 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala @@ -0,0 +1,58 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator + +sealed trait FactoryError: + def entityId: String + def userMessage: UserMessage + +object FactoryError: + case class EntityAlreadyExists[Id](entityId: String, id: Id) + extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) + + case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) + +trait AggregateRootFactory[ + Ident, + T <: AggregateRoot[Ident, ?, ?, ?, ?] +]: + type Id = Ident + type NotFound = FactoryError.EntityNotFound[Id] + type AlreadyExists = FactoryError.EntityAlreadyExists[Id] + + protected def AlreadyExists(id: Id): AlreadyExists = + FactoryError.EntityAlreadyExists(entityId, id) + + protected def NotFound(id: Id): NotFound = + FactoryError.EntityNotFound(entityId, id) + + def entityId: String + def make(id: Id): IO[AlreadyExists, T] + def load(id: Id): IO[NotFound, T] + + def loadOrMake(id: Id): UIO[T] = + load(id) + .orElse(make(id)) + .orDieWith(_ => + new RuntimeException( + s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" + ) + ) + + def make()(using idGen: IdGenerator[Id]): UIO[T] = + val tryToCreate = for + id <- idGen.nextId + entity <- make(id) + yield entity + + tryToCreate + .retryN(10) + .orDieWith(_ => + new RuntimeException( + "Cannot create entity, generator failed 10 times to create new ID" + ) + ) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala new file mode 100644 index 0000000..b2fef3a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala @@ -0,0 +1,24 @@ +package works.iterative.entity + +import works.iterative.core.Validated +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator +import zio.* + +/** Generic class for entity factories. + * + * Creates a new entity from a "seed". + */ +trait EntityFactory[Entity, Seed]: + def make(seed: Seed): IO[UserMessage, Entity] + +/** A factory using idGenerator to create the entity */ +class GeneratingFactory[Entity, Seed, Id]( + constructor: Seed => Validated[Id => Entity], + idGenerator: IdGenerator[Id] +) extends EntityFactory[Entity, Seed]: + override def make(seed: Seed): IO[UserMessage, Entity] = + for + construct <- constructor(seed).toZIO + id <- idGenerator.nextId + yield construct(id) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala new file mode 100644 index 0000000..29ae384 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.auth.CurrentUser + +trait EntityUpdateService[Id, Command, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def update(id: Id, command: Command): Op[Unit] + +trait EntityCreateService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(initData: Init): Op[Id] + +trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(id: Id, initData: Init): Op[Id] + +trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] + extends EntityCreateService[Id, Init, Error] + with EntityUpdateService[Id, Command, Error] diff --git a/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala new file mode 100644 index 0000000..946011a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import zio.* + +trait ViewProcessor[Event]: + def process(event: Event): UIO[Unit] diff --git a/entity/shared/src/main/scala/works/iterative/event/Event.scala b/entity/shared/src/main/scala/works/iterative/event/Event.scala new file mode 100644 index 0000000..7907ac0 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/Event.scala @@ -0,0 +1,4 @@ +package works.iterative.event + +trait Event[A]: + def record: EventRecord diff --git a/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala new file mode 100644 index 0000000..490417a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala @@ -0,0 +1,15 @@ +package works.iterative.event + +import works.iterative.core.* +import zio.* + +/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ +final case class EventRecord(userHandle: UserHandle, timestamp: Moment): + val displayName: String = userHandle.displayName + +object EventRecord: + def apply(userHandle: UserHandle): UIO[EventRecord] = + Moment.now.map(EventRecord(userHandle, _)) + + def now(using userHandle: UserHandle): UIO[EventRecord] = + apply(userHandle) diff --git a/entity/shared/src/main/scala/works/iterative/event/EventStore.scala b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala new file mode 100644 index 0000000..3226df3 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala @@ -0,0 +1,13 @@ +package works.iterative.event + +import zio.* + +/** Abstraction of event log + * + * The log can persist events and restore the state of an entity from the + * events. + */ +trait EventStore[Id, T <: Event[_]]: + type Op[A] = UIO[A] + def persist(id: Id, event: T): Op[Unit] + def get(id: Id): Op[Seq[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala new file mode 100644 index 0000000..6ccccd7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala @@ -0,0 +1,12 @@ +package works.iterative.event + +import zio.* + +/** Snapshot store + * + * Stores a snapshot of an entity. + */ +trait SnapshotStore[Id, T]: + type Op[A] = UIO[A] + def update(id: Id, snapshot: T): Op[Unit] + def get(id: Id): Op[Option[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala new file mode 100644 index 0000000..964d420 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala @@ -0,0 +1,21 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) + extends EventStore[Id, T]: + override def persist(id: Id, event: T): Op[Unit] = + data.update { m => + val events = m.getOrElse(id, Nil) + m.updated(id, event :: events) + }.unit + override def get(id: Id): Op[Seq[T]] = + data.get.map(_.getOrElse(id, Nil)) + +object InMemoryEventStore: + def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, List[T]]) + yield InMemoryEventStore(data) + } diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala new file mode 100644 index 0000000..722d7d7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala @@ -0,0 +1,19 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) + extends SnapshotStore[Id, T]: + override def update(id: Id, snapshot: T): UIO[Unit] = + data.update(_.updated(id, snapshot)).unit + + override def get(id: Id): UIO[Option[T]] = + data.get.map(_.get(id)) + +object InMemorySnapshotStore: + def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, T]) + yield InMemorySnapshotStore(data) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala new file mode 100644 index 0000000..874d16c --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala @@ -0,0 +1,63 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import sttp.client3.* + +class ConsulKeyValueStore( + token: ConsulToken, + baseUri: BaseUri, + backend: CustomTapir.Backend +) extends KeyValueStore[String, String]: + + private val defaultBase = "http://localhost:8500" + private val base = baseUri.value.map(_.toString).getOrElse(defaultBase) + + private def addAuth[U[_], T, R]( + request: RequestT[U, T, R] + ): RequestT[U, T, R] = + request.header("X-Consul-Token", token.token) + + override def get(key: String): UIO[Option[String]] = { + for + response <- addAuth(basicRequest.get(uri"$base/v1/kv/${key}?raw=true")) + .send(backend) + yield response.body.toOption + }.orDie + + override def put(key: String, value: String): UIO[Unit] = { + addAuth(basicRequest.put(uri"$base/v1/kv/${key}").body(value)) + .send(backend) + .unit + }.orDie + + override def remove(key: String): UIO[Unit] = { + addAuth(basicRequest.delete(uri"$base/v1/kv/${key}")) + .send(backend) + .unit + }.orDie + +object ConsulKeyValueStore: + val layer: URLayer[ + ConsulToken & BaseUri & CustomTapir.Backend, + StringKeyValueStore + ] = + ZLayer { + for + token <- ZIO.service[ConsulToken] + baseUri <- ZIO.service[BaseUri] + backend <- ZIO.service[CustomTapir.Backend] + yield ConsulKeyValueStore(token, baseUri, backend) + } + + val fromEnv + : ZLayer[CustomTapir.Backend, SecurityException, StringKeyValueStore] = + ConsulToken.fromEnv >>> ZLayer { + for + token <- ZIO.service[ConsulToken] + backend <- ZIO.service[CustomTapir.Backend] + addr <- System.envOrElse("CONSUL_ADDR", "http://localhost:8500") + yield ConsulKeyValueStore(token, BaseUri(addr), backend) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala new file mode 100644 index 0000000..5c5483a --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala @@ -0,0 +1,150 @@ +package works.iterative.core.service.impl + +import zio.* +import sttp.tapir.EndpointIO.annotations.* +import sttp.tapir.Schema +import zio.json.* + +final case class ConsulToken(token: Option[String]) + +object ConsulToken: + def fromEnv: ZLayer[Any, SecurityException, ConsulToken] = ZLayer { + System.env("CONSUL_TOKEN").map(ConsulToken(_)) + } + +@jsonMemberNames(PascalCase) +final case class ConsulMetadata( + createIndex: Long, + modifyIndex: Long, + lockIndex: Long, + key: String, + flags: Long, + value: String, + session: Option[String] +) derives JsonCodec, + Schema + +@endpointInput("v1/kv/{key}") +final case class GetQuery( + @path + @description("Key to get from the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + // TODO: This would not work type-wise properly, as the result type of the call would differ depending on the query parameters + // These are missing, split to different queries to enable. + /* + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies the response is just the raw value of the key, without any encoding or metadata." + ) + raw: Option[Boolean] = None, + @query + @description( + "Specifies to return only keys (no values or metadata). Specifying this parameter implies recurse." + ) + keys: Option[Boolean] = None, + @query + @description( + "Specifies the string to use as a separator for recursive key lookups. This option is only used when paired with the keys parameter to limit the prefix of keys returned, only up to the given separator." + ) + separator: Option[String] = None, + */ + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class PutQuery( + @path + @description("Key to put into the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies an unsigned value between 0 and (2^64)-1 to store with the key. API consumers can use this field any way they choose for their application." + ) + flags: Option[Long] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Supply a session ID to use in a lock acquisition operation.") + acquire: Option[String] = None, + @query + @description("Supply a session ID to use in a release operation.") + release: Option[String] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class DeleteQuery( + @path + @description("Key to delete in the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +/* +sealed trait ConsulGetError +object ConsulGetError: + case object NotFound extends ConsulGetError derives JsonCodec, Schema + case object Forbidden extends ConsulGetError derives JsonCodec, Schema + case class ServerError(msg: String) extends ConsulGetError derives JsonCodec, Schema + +trait ConsulKVEndpoints extends CustomTapir: + private val kvBase = endpoint + .in("v1" / "kv") + .securityIn( + auth.apiKey(header[Option[String]]("X-Consul-Token").mapTo[ConsulToken]) + ) + + val getKV: Endpoint[ConsulToken, GetQuery, Unit, ConsulMetadata, Any] = + kvBase.get + .in(EndpointInput.derived[GetQuery]) + .out(jsonBody[ConsulMetadata]) + .errorOut( + oneOf[ConsulGetError]( + oneOfVariant( + statusCode(StatusCode.NotFound).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfVariant( + statusCode(StatusCode.Forbidden).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfDefaultVariant( + statusCode(StatusCode.InternalServerError).and(jsonBody[ServerError].description("not found")) + ) + ) + ) + val putKV: Endpoint[ConsulToken, PutQuery, Unit, Boolean, Any] = + kvBase.put.in(EndpointInput.derived[PutQuery]).out(jsonBody[Boolean]) + val deleteKV: Endpoint[ConsulToken, DeleteQuery, Unit, Boolean, Any] = + kvBase.delete.in(EndpointInput.derived[DeleteQuery]).out(jsonBody[Boolean]) + +*/ \ No newline at end of file diff --git a/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala new file mode 100644 index 0000000..c938083 --- /dev/null +++ b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala @@ -0,0 +1,19 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import zio.test.* + +object ConsulKeyValueStoreSpec extends specs.KeyValueStoreSpec: + override def spec = defaultSpec.provide( + ConsulKeyValueStore.layer, + CustomTapir.clientLayer, + ZLayer { + Live.live(System.env("CONSUL_ADDR")).map(a => BaseUri(a.get)) + }, + ZLayer { + Live.live(System.env("CONSUL_TOKEN")).map(ConsulToken(_)) + } + ) @@ TestAspect.ifEnvSet("CONSUL_ADDR") diff --git a/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala new file mode 100644 index 0000000..16211a8 --- /dev/null +++ b/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala @@ -0,0 +1,29 @@ +package works.iterative.core.service.impl + +import org.scalajs.dom.Storage +import works.iterative.core.service.Repository +import zio.* +import zio.json.* + +// TODO: improve error reporting on generic repositories +// This is good just for prototypes +// And it cannot be used to query data, as there is no way to iterate the storage +class JsStorageRepository[Value: JsonCodec]( + storage: Storage +) extends Repository[String, Value, String]: + + override def load(id: String): UIO[Option[Value]] = { + for + raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) + data <- ZIO.foreach(raw) { r => + ZIO + .fromEither(r.fromJson[Value]) + .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) + } + yield data + }.orDie + + override def save(key: String, value: Value): UIO[Unit] = + ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie + + override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala new file mode 100644 index 0000000..8c88818 --- /dev/null +++ b/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala @@ -0,0 +1,26 @@ +package works.iterative.core +package service +package impl + +import zio.* +import zio.stream.* + +class JcaDigestService extends DigestService: + + override def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): IO[E, Digest] = + for + md <- ZIO + .attempt( + java.security.MessageDigest.getInstance(algorithm.value) + ) + .orDie + _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) + digest <- ZIO.attempt(md.digest()).orDie + yield Digest(algorithm, digest) + +object JcaDigestGenerator: + val layer: ULayer[DigestService] = + ZLayer.succeed(new JcaDigestService) diff --git a/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala new file mode 100644 index 0000000..883bd7a --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala @@ -0,0 +1,26 @@ +package works.iterative.core.service + +import works.iterative.core.Digest +import works.iterative.core.DigestAlgorithm + +import zio.* +import zio.stream.* + +trait DigestService: + def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): IO[E, Digest] + +object DigestService: + def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): ZIO[DigestService, E, Digest] = + ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) + + def digest( + algorithm: DigestAlgorithm, + content: Array[Byte] + ): URIO[DigestService, Digest] = + digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala new file mode 100644 index 0000000..10bdeb7 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala @@ -0,0 +1,29 @@ +package works.iterative.core.service + +import works.iterative.core.FileSupport.* +import works.iterative.core.FileRef + +import zio.* + +trait FileStore: + type Op[A] = UIO[A] + def store(file: FileRepr): Op[FileRef] + def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] + def load(url: String): Op[Option[Array[Byte]]] + +object FileStore: + type Op[A] = URIO[FileStore, A] + def store(file: FileRepr): Op[FileRef] = + ZIO.serviceWithZIO(_.store(file)) + def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] = + ZIO.serviceWithZIO(_.store(name, file, contentType)) + def load(url: String): Op[Option[Array[Byte]]] = + ZIO.serviceWithZIO(_.load(url)) diff --git a/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala new file mode 100644 index 0000000..1779da3 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala @@ -0,0 +1,10 @@ +package works.iterative.core.service + +import zio.* + +/** Generator of unique IDs of a given type */ +trait IdGenerator[A]: + self => + def nextId: UIO[A] + def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: + def nextId: UIO[B] = self.nextId.map(f) diff --git a/service/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/service/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala new file mode 100644 index 0000000..23d1908 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala @@ -0,0 +1,47 @@ +package works.iterative.core.service + +import zio.* +import zio.json.* + +trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: + type Op[A] = Eff[A] + def get(key: Key): Eff[Option[Value]] + +trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: + def put(key: Key, value: Value): Eff[Unit] + def remove(key: Key): Eff[Unit] + +trait GenericKeyValueStore[Eff[+_], -Key, Value] + extends GenericReadKeyValueStore[Eff, Key, Value] + with GenericWriteKeyValueStore[Eff, Key, Value] + +trait ReadKeyValueStore[-Key, +Value] + extends GenericReadKeyValueStore[UIO, Key, Value] + +trait WriteKeyValueStore[-Key, -Value] + extends GenericWriteKeyValueStore[UIO, Key, Value] + +trait KeyValueStore[-Key, Value] + extends GenericKeyValueStore[UIO, Key, Value] + with ReadKeyValueStore[Key, Value] + with WriteKeyValueStore[Key, Value] + +type StringKeyValueStore = KeyValueStore[String, String] + +object KeyValueStore: + extension (store: StringKeyValueStore) + /** Decode the value, ignoring decoding errors if any */ + def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = + store.get(key).map(_.flatMap(_.fromJson[A].toOption)) + + def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = + store.get(key).flatMap { + case None => ZIO.none + case Some(v) => + ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => + ZIO.log(err) *> ZIO.none + } + } + + def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = + store.put(key, value.toJson) diff --git a/service/shared/src/main/scala/works/iterative/core/service/Repository.scala b/service/shared/src/main/scala/works/iterative/core/service/Repository.scala new file mode 100644 index 0000000..c450279 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/Repository.scala @@ -0,0 +1,52 @@ +package works.iterative.core.service + +import zio.* +import zio.stream.* + +trait GenericLoadService[Eff[+_], -Key, +Value]: + type Op[A] = Eff[A] + def load(id: Key): Op[Option[Value]] + +trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: + def updates: Eff[Str[Key]] + +trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: + type Op[A] = Eff[A] + def loadAll(ids: Seq[Key]): Op[Coll[Value]] + +trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: + type Op[A] = Eff[A] + def find(filter: FilterArg): Op[Coll[Value]] + +trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] + extends GenericLoadService[Eff, Key, Value] + with GenericLoadAllService[Eff, Coll, Key, Value] + with GenericFindService[Eff, Coll, Key, Value, FilterArg] + +trait GenericWriteRepository[Eff[_], -Key, -Value]: + type Op[A] = Eff[A] + def save(key: Key, value: Value): Op[Unit] + +trait GenericRepository[Eff[+_], -Key, Value] + extends GenericReadRepository[Eff, List, Key, Value, Unit] + with GenericWriteRepository[Eff, Key, Value] + +type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] +type LoadAllRepository[-Key, +Value] = + GenericLoadAllService[UIO, List, Key, Value] + +trait ReadRepository[-Key, +Value, -FilterArg] + extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: + override def loadAll(ids: Seq[Key]): UIO[List[Value]] = + // Inefficient implementation, meant to be overridden + ZIO.foreach(ids)(load).map(_.flatten.toList) + +trait UpdateNotifyRepository[Key] + extends GenericUpdateNotifyService[UIO, UStream, Key] + +trait WriteRepository[-Key, -Value] + extends GenericWriteRepository[UIO, Key, Value] + +trait Repository[-Key, Value, -FilterArg] + extends ReadRepository[Key, Value, FilterArg] + with WriteRepository[Key, Value] diff --git a/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala new file mode 100644 index 0000000..66ad66b --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala @@ -0,0 +1,21 @@ +package works.iterative.core.service +package impl + +import works.iterative.core.FileSupport.* +import works.iterative.core.FileRef +import zio.* + +object InMemoryFileStore: + val layer: ULayer[FileStore] = ZLayer.succeed { + new FileStore: + override def store(file: FileRepr): Op[FileRef] = + ZIO.succeed(FileRef.unsafe(file.name, "#")) + override def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] = + ZIO.succeed(FileRef.unsafe(name, "#")) + override def load(url: String): Op[Option[Array[Byte]]] = + ZIO.succeed(None) + } diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala new file mode 100644 index 0000000..65c7e92 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* + +/** Represents aggregate root in DDD + * + * @tparam Id + * Aggregate root id + * @tparam Command + * Aggregate root command + * @tparam Event + * Aggregate root event + * @tparam State + * Aggregate root state + */ +trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: + /** Aggregate root id */ + def id: Id + + /** Current state */ + def state: UIO[State] + + def handle(command: Command): IO[Error, Unit] diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala new file mode 100644 index 0000000..c051650 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala @@ -0,0 +1,58 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator + +sealed trait FactoryError: + def entityId: String + def userMessage: UserMessage + +object FactoryError: + case class EntityAlreadyExists[Id](entityId: String, id: Id) + extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) + + case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) + +trait AggregateRootFactory[ + Ident, + T <: AggregateRoot[Ident, ?, ?, ?, ?] +]: + type Id = Ident + type NotFound = FactoryError.EntityNotFound[Id] + type AlreadyExists = FactoryError.EntityAlreadyExists[Id] + + protected def AlreadyExists(id: Id): AlreadyExists = + FactoryError.EntityAlreadyExists(entityId, id) + + protected def NotFound(id: Id): NotFound = + FactoryError.EntityNotFound(entityId, id) + + def entityId: String + def make(id: Id): IO[AlreadyExists, T] + def load(id: Id): IO[NotFound, T] + + def loadOrMake(id: Id): UIO[T] = + load(id) + .orElse(make(id)) + .orDieWith(_ => + new RuntimeException( + s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" + ) + ) + + def make()(using idGen: IdGenerator[Id]): UIO[T] = + val tryToCreate = for + id <- idGen.nextId + entity <- make(id) + yield entity + + tryToCreate + .retryN(10) + .orDieWith(_ => + new RuntimeException( + "Cannot create entity, generator failed 10 times to create new ID" + ) + ) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala new file mode 100644 index 0000000..b2fef3a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala @@ -0,0 +1,24 @@ +package works.iterative.entity + +import works.iterative.core.Validated +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator +import zio.* + +/** Generic class for entity factories. + * + * Creates a new entity from a "seed". + */ +trait EntityFactory[Entity, Seed]: + def make(seed: Seed): IO[UserMessage, Entity] + +/** A factory using idGenerator to create the entity */ +class GeneratingFactory[Entity, Seed, Id]( + constructor: Seed => Validated[Id => Entity], + idGenerator: IdGenerator[Id] +) extends EntityFactory[Entity, Seed]: + override def make(seed: Seed): IO[UserMessage, Entity] = + for + construct <- constructor(seed).toZIO + id <- idGenerator.nextId + yield construct(id) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala new file mode 100644 index 0000000..29ae384 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.auth.CurrentUser + +trait EntityUpdateService[Id, Command, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def update(id: Id, command: Command): Op[Unit] + +trait EntityCreateService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(initData: Init): Op[Id] + +trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(id: Id, initData: Init): Op[Id] + +trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] + extends EntityCreateService[Id, Init, Error] + with EntityUpdateService[Id, Command, Error] diff --git a/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala new file mode 100644 index 0000000..946011a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import zio.* + +trait ViewProcessor[Event]: + def process(event: Event): UIO[Unit] diff --git a/entity/shared/src/main/scala/works/iterative/event/Event.scala b/entity/shared/src/main/scala/works/iterative/event/Event.scala new file mode 100644 index 0000000..7907ac0 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/Event.scala @@ -0,0 +1,4 @@ +package works.iterative.event + +trait Event[A]: + def record: EventRecord diff --git a/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala new file mode 100644 index 0000000..490417a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala @@ -0,0 +1,15 @@ +package works.iterative.event + +import works.iterative.core.* +import zio.* + +/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ +final case class EventRecord(userHandle: UserHandle, timestamp: Moment): + val displayName: String = userHandle.displayName + +object EventRecord: + def apply(userHandle: UserHandle): UIO[EventRecord] = + Moment.now.map(EventRecord(userHandle, _)) + + def now(using userHandle: UserHandle): UIO[EventRecord] = + apply(userHandle) diff --git a/entity/shared/src/main/scala/works/iterative/event/EventStore.scala b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala new file mode 100644 index 0000000..3226df3 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala @@ -0,0 +1,13 @@ +package works.iterative.event + +import zio.* + +/** Abstraction of event log + * + * The log can persist events and restore the state of an entity from the + * events. + */ +trait EventStore[Id, T <: Event[_]]: + type Op[A] = UIO[A] + def persist(id: Id, event: T): Op[Unit] + def get(id: Id): Op[Seq[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala new file mode 100644 index 0000000..6ccccd7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala @@ -0,0 +1,12 @@ +package works.iterative.event + +import zio.* + +/** Snapshot store + * + * Stores a snapshot of an entity. + */ +trait SnapshotStore[Id, T]: + type Op[A] = UIO[A] + def update(id: Id, snapshot: T): Op[Unit] + def get(id: Id): Op[Option[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala new file mode 100644 index 0000000..964d420 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala @@ -0,0 +1,21 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) + extends EventStore[Id, T]: + override def persist(id: Id, event: T): Op[Unit] = + data.update { m => + val events = m.getOrElse(id, Nil) + m.updated(id, event :: events) + }.unit + override def get(id: Id): Op[Seq[T]] = + data.get.map(_.getOrElse(id, Nil)) + +object InMemoryEventStore: + def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, List[T]]) + yield InMemoryEventStore(data) + } diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala new file mode 100644 index 0000000..722d7d7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala @@ -0,0 +1,19 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) + extends SnapshotStore[Id, T]: + override def update(id: Id, snapshot: T): UIO[Unit] = + data.update(_.updated(id, snapshot)).unit + + override def get(id: Id): UIO[Option[T]] = + data.get.map(_.get(id)) + +object InMemorySnapshotStore: + def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, T]) + yield InMemorySnapshotStore(data) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala new file mode 100644 index 0000000..874d16c --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala @@ -0,0 +1,63 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import sttp.client3.* + +class ConsulKeyValueStore( + token: ConsulToken, + baseUri: BaseUri, + backend: CustomTapir.Backend +) extends KeyValueStore[String, String]: + + private val defaultBase = "http://localhost:8500" + private val base = baseUri.value.map(_.toString).getOrElse(defaultBase) + + private def addAuth[U[_], T, R]( + request: RequestT[U, T, R] + ): RequestT[U, T, R] = + request.header("X-Consul-Token", token.token) + + override def get(key: String): UIO[Option[String]] = { + for + response <- addAuth(basicRequest.get(uri"$base/v1/kv/${key}?raw=true")) + .send(backend) + yield response.body.toOption + }.orDie + + override def put(key: String, value: String): UIO[Unit] = { + addAuth(basicRequest.put(uri"$base/v1/kv/${key}").body(value)) + .send(backend) + .unit + }.orDie + + override def remove(key: String): UIO[Unit] = { + addAuth(basicRequest.delete(uri"$base/v1/kv/${key}")) + .send(backend) + .unit + }.orDie + +object ConsulKeyValueStore: + val layer: URLayer[ + ConsulToken & BaseUri & CustomTapir.Backend, + StringKeyValueStore + ] = + ZLayer { + for + token <- ZIO.service[ConsulToken] + baseUri <- ZIO.service[BaseUri] + backend <- ZIO.service[CustomTapir.Backend] + yield ConsulKeyValueStore(token, baseUri, backend) + } + + val fromEnv + : ZLayer[CustomTapir.Backend, SecurityException, StringKeyValueStore] = + ConsulToken.fromEnv >>> ZLayer { + for + token <- ZIO.service[ConsulToken] + backend <- ZIO.service[CustomTapir.Backend] + addr <- System.envOrElse("CONSUL_ADDR", "http://localhost:8500") + yield ConsulKeyValueStore(token, BaseUri(addr), backend) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala new file mode 100644 index 0000000..5c5483a --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala @@ -0,0 +1,150 @@ +package works.iterative.core.service.impl + +import zio.* +import sttp.tapir.EndpointIO.annotations.* +import sttp.tapir.Schema +import zio.json.* + +final case class ConsulToken(token: Option[String]) + +object ConsulToken: + def fromEnv: ZLayer[Any, SecurityException, ConsulToken] = ZLayer { + System.env("CONSUL_TOKEN").map(ConsulToken(_)) + } + +@jsonMemberNames(PascalCase) +final case class ConsulMetadata( + createIndex: Long, + modifyIndex: Long, + lockIndex: Long, + key: String, + flags: Long, + value: String, + session: Option[String] +) derives JsonCodec, + Schema + +@endpointInput("v1/kv/{key}") +final case class GetQuery( + @path + @description("Key to get from the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + // TODO: This would not work type-wise properly, as the result type of the call would differ depending on the query parameters + // These are missing, split to different queries to enable. + /* + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies the response is just the raw value of the key, without any encoding or metadata." + ) + raw: Option[Boolean] = None, + @query + @description( + "Specifies to return only keys (no values or metadata). Specifying this parameter implies recurse." + ) + keys: Option[Boolean] = None, + @query + @description( + "Specifies the string to use as a separator for recursive key lookups. This option is only used when paired with the keys parameter to limit the prefix of keys returned, only up to the given separator." + ) + separator: Option[String] = None, + */ + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class PutQuery( + @path + @description("Key to put into the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies an unsigned value between 0 and (2^64)-1 to store with the key. API consumers can use this field any way they choose for their application." + ) + flags: Option[Long] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Supply a session ID to use in a lock acquisition operation.") + acquire: Option[String] = None, + @query + @description("Supply a session ID to use in a release operation.") + release: Option[String] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class DeleteQuery( + @path + @description("Key to delete in the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +/* +sealed trait ConsulGetError +object ConsulGetError: + case object NotFound extends ConsulGetError derives JsonCodec, Schema + case object Forbidden extends ConsulGetError derives JsonCodec, Schema + case class ServerError(msg: String) extends ConsulGetError derives JsonCodec, Schema + +trait ConsulKVEndpoints extends CustomTapir: + private val kvBase = endpoint + .in("v1" / "kv") + .securityIn( + auth.apiKey(header[Option[String]]("X-Consul-Token").mapTo[ConsulToken]) + ) + + val getKV: Endpoint[ConsulToken, GetQuery, Unit, ConsulMetadata, Any] = + kvBase.get + .in(EndpointInput.derived[GetQuery]) + .out(jsonBody[ConsulMetadata]) + .errorOut( + oneOf[ConsulGetError]( + oneOfVariant( + statusCode(StatusCode.NotFound).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfVariant( + statusCode(StatusCode.Forbidden).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfDefaultVariant( + statusCode(StatusCode.InternalServerError).and(jsonBody[ServerError].description("not found")) + ) + ) + ) + val putKV: Endpoint[ConsulToken, PutQuery, Unit, Boolean, Any] = + kvBase.put.in(EndpointInput.derived[PutQuery]).out(jsonBody[Boolean]) + val deleteKV: Endpoint[ConsulToken, DeleteQuery, Unit, Boolean, Any] = + kvBase.delete.in(EndpointInput.derived[DeleteQuery]).out(jsonBody[Boolean]) + +*/ \ No newline at end of file diff --git a/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala new file mode 100644 index 0000000..c938083 --- /dev/null +++ b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala @@ -0,0 +1,19 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import zio.test.* + +object ConsulKeyValueStoreSpec extends specs.KeyValueStoreSpec: + override def spec = defaultSpec.provide( + ConsulKeyValueStore.layer, + CustomTapir.clientLayer, + ZLayer { + Live.live(System.env("CONSUL_ADDR")).map(a => BaseUri(a.get)) + }, + ZLayer { + Live.live(System.env("CONSUL_TOKEN")).map(ConsulToken(_)) + } + ) @@ TestAspect.ifEnvSet("CONSUL_ADDR") diff --git a/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala new file mode 100644 index 0000000..16211a8 --- /dev/null +++ b/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala @@ -0,0 +1,29 @@ +package works.iterative.core.service.impl + +import org.scalajs.dom.Storage +import works.iterative.core.service.Repository +import zio.* +import zio.json.* + +// TODO: improve error reporting on generic repositories +// This is good just for prototypes +// And it cannot be used to query data, as there is no way to iterate the storage +class JsStorageRepository[Value: JsonCodec]( + storage: Storage +) extends Repository[String, Value, String]: + + override def load(id: String): UIO[Option[Value]] = { + for + raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) + data <- ZIO.foreach(raw) { r => + ZIO + .fromEither(r.fromJson[Value]) + .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) + } + yield data + }.orDie + + override def save(key: String, value: Value): UIO[Unit] = + ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie + + override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala new file mode 100644 index 0000000..8c88818 --- /dev/null +++ b/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala @@ -0,0 +1,26 @@ +package works.iterative.core +package service +package impl + +import zio.* +import zio.stream.* + +class JcaDigestService extends DigestService: + + override def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): IO[E, Digest] = + for + md <- ZIO + .attempt( + java.security.MessageDigest.getInstance(algorithm.value) + ) + .orDie + _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) + digest <- ZIO.attempt(md.digest()).orDie + yield Digest(algorithm, digest) + +object JcaDigestGenerator: + val layer: ULayer[DigestService] = + ZLayer.succeed(new JcaDigestService) diff --git a/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala new file mode 100644 index 0000000..883bd7a --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala @@ -0,0 +1,26 @@ +package works.iterative.core.service + +import works.iterative.core.Digest +import works.iterative.core.DigestAlgorithm + +import zio.* +import zio.stream.* + +trait DigestService: + def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): IO[E, Digest] + +object DigestService: + def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): ZIO[DigestService, E, Digest] = + ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) + + def digest( + algorithm: DigestAlgorithm, + content: Array[Byte] + ): URIO[DigestService, Digest] = + digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala new file mode 100644 index 0000000..10bdeb7 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala @@ -0,0 +1,29 @@ +package works.iterative.core.service + +import works.iterative.core.FileSupport.* +import works.iterative.core.FileRef + +import zio.* + +trait FileStore: + type Op[A] = UIO[A] + def store(file: FileRepr): Op[FileRef] + def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] + def load(url: String): Op[Option[Array[Byte]]] + +object FileStore: + type Op[A] = URIO[FileStore, A] + def store(file: FileRepr): Op[FileRef] = + ZIO.serviceWithZIO(_.store(file)) + def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] = + ZIO.serviceWithZIO(_.store(name, file, contentType)) + def load(url: String): Op[Option[Array[Byte]]] = + ZIO.serviceWithZIO(_.load(url)) diff --git a/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala new file mode 100644 index 0000000..1779da3 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala @@ -0,0 +1,10 @@ +package works.iterative.core.service + +import zio.* + +/** Generator of unique IDs of a given type */ +trait IdGenerator[A]: + self => + def nextId: UIO[A] + def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: + def nextId: UIO[B] = self.nextId.map(f) diff --git a/service/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/service/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala new file mode 100644 index 0000000..23d1908 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala @@ -0,0 +1,47 @@ +package works.iterative.core.service + +import zio.* +import zio.json.* + +trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: + type Op[A] = Eff[A] + def get(key: Key): Eff[Option[Value]] + +trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: + def put(key: Key, value: Value): Eff[Unit] + def remove(key: Key): Eff[Unit] + +trait GenericKeyValueStore[Eff[+_], -Key, Value] + extends GenericReadKeyValueStore[Eff, Key, Value] + with GenericWriteKeyValueStore[Eff, Key, Value] + +trait ReadKeyValueStore[-Key, +Value] + extends GenericReadKeyValueStore[UIO, Key, Value] + +trait WriteKeyValueStore[-Key, -Value] + extends GenericWriteKeyValueStore[UIO, Key, Value] + +trait KeyValueStore[-Key, Value] + extends GenericKeyValueStore[UIO, Key, Value] + with ReadKeyValueStore[Key, Value] + with WriteKeyValueStore[Key, Value] + +type StringKeyValueStore = KeyValueStore[String, String] + +object KeyValueStore: + extension (store: StringKeyValueStore) + /** Decode the value, ignoring decoding errors if any */ + def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = + store.get(key).map(_.flatMap(_.fromJson[A].toOption)) + + def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = + store.get(key).flatMap { + case None => ZIO.none + case Some(v) => + ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => + ZIO.log(err) *> ZIO.none + } + } + + def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = + store.put(key, value.toJson) diff --git a/service/shared/src/main/scala/works/iterative/core/service/Repository.scala b/service/shared/src/main/scala/works/iterative/core/service/Repository.scala new file mode 100644 index 0000000..c450279 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/Repository.scala @@ -0,0 +1,52 @@ +package works.iterative.core.service + +import zio.* +import zio.stream.* + +trait GenericLoadService[Eff[+_], -Key, +Value]: + type Op[A] = Eff[A] + def load(id: Key): Op[Option[Value]] + +trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: + def updates: Eff[Str[Key]] + +trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: + type Op[A] = Eff[A] + def loadAll(ids: Seq[Key]): Op[Coll[Value]] + +trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: + type Op[A] = Eff[A] + def find(filter: FilterArg): Op[Coll[Value]] + +trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] + extends GenericLoadService[Eff, Key, Value] + with GenericLoadAllService[Eff, Coll, Key, Value] + with GenericFindService[Eff, Coll, Key, Value, FilterArg] + +trait GenericWriteRepository[Eff[_], -Key, -Value]: + type Op[A] = Eff[A] + def save(key: Key, value: Value): Op[Unit] + +trait GenericRepository[Eff[+_], -Key, Value] + extends GenericReadRepository[Eff, List, Key, Value, Unit] + with GenericWriteRepository[Eff, Key, Value] + +type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] +type LoadAllRepository[-Key, +Value] = + GenericLoadAllService[UIO, List, Key, Value] + +trait ReadRepository[-Key, +Value, -FilterArg] + extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: + override def loadAll(ids: Seq[Key]): UIO[List[Value]] = + // Inefficient implementation, meant to be overridden + ZIO.foreach(ids)(load).map(_.flatten.toList) + +trait UpdateNotifyRepository[Key] + extends GenericUpdateNotifyService[UIO, UStream, Key] + +trait WriteRepository[-Key, -Value] + extends GenericWriteRepository[UIO, Key, Value] + +trait Repository[-Key, Value, -FilterArg] + extends ReadRepository[Key, Value, FilterArg] + with WriteRepository[Key, Value] diff --git a/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala new file mode 100644 index 0000000..66ad66b --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala @@ -0,0 +1,21 @@ +package works.iterative.core.service +package impl + +import works.iterative.core.FileSupport.* +import works.iterative.core.FileRef +import zio.* + +object InMemoryFileStore: + val layer: ULayer[FileStore] = ZLayer.succeed { + new FileStore: + override def store(file: FileRepr): Op[FileRef] = + ZIO.succeed(FileRef.unsafe(file.name, "#")) + override def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] = + ZIO.succeed(FileRef.unsafe(name, "#")) + override def load(url: String): Op[Option[Array[Byte]]] = + ZIO.succeed(None) + } diff --git a/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala new file mode 100644 index 0000000..e3b7d45 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala @@ -0,0 +1,17 @@ +package works.iterative.core.service +package impl + +import zio.* + +trait InMemoryRepository[Key, Value, FilterArg]( + data: Ref[Map[Key, Value]], + filter: FilterArg => Value => Boolean +) extends Repository[Key, Value, FilterArg]: + override def save(key: Key, value: Value): UIO[Unit] = + data.update(_ + (key -> value)) + override def load(key: Key): UIO[Option[Value]] = + data.get.map(_.get(key)) + override def loadAll(keys: Seq[Key]): UIO[List[Value]] = + data.get.map(_.view.filterKeys(keys.contains).values.toList) + override def find(filterArg: FilterArg): UIO[List[Value]] = + data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala new file mode 100644 index 0000000..65c7e92 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* + +/** Represents aggregate root in DDD + * + * @tparam Id + * Aggregate root id + * @tparam Command + * Aggregate root command + * @tparam Event + * Aggregate root event + * @tparam State + * Aggregate root state + */ +trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: + /** Aggregate root id */ + def id: Id + + /** Current state */ + def state: UIO[State] + + def handle(command: Command): IO[Error, Unit] diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala new file mode 100644 index 0000000..c051650 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala @@ -0,0 +1,58 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator + +sealed trait FactoryError: + def entityId: String + def userMessage: UserMessage + +object FactoryError: + case class EntityAlreadyExists[Id](entityId: String, id: Id) + extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) + + case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) + +trait AggregateRootFactory[ + Ident, + T <: AggregateRoot[Ident, ?, ?, ?, ?] +]: + type Id = Ident + type NotFound = FactoryError.EntityNotFound[Id] + type AlreadyExists = FactoryError.EntityAlreadyExists[Id] + + protected def AlreadyExists(id: Id): AlreadyExists = + FactoryError.EntityAlreadyExists(entityId, id) + + protected def NotFound(id: Id): NotFound = + FactoryError.EntityNotFound(entityId, id) + + def entityId: String + def make(id: Id): IO[AlreadyExists, T] + def load(id: Id): IO[NotFound, T] + + def loadOrMake(id: Id): UIO[T] = + load(id) + .orElse(make(id)) + .orDieWith(_ => + new RuntimeException( + s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" + ) + ) + + def make()(using idGen: IdGenerator[Id]): UIO[T] = + val tryToCreate = for + id <- idGen.nextId + entity <- make(id) + yield entity + + tryToCreate + .retryN(10) + .orDieWith(_ => + new RuntimeException( + "Cannot create entity, generator failed 10 times to create new ID" + ) + ) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala new file mode 100644 index 0000000..b2fef3a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala @@ -0,0 +1,24 @@ +package works.iterative.entity + +import works.iterative.core.Validated +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator +import zio.* + +/** Generic class for entity factories. + * + * Creates a new entity from a "seed". + */ +trait EntityFactory[Entity, Seed]: + def make(seed: Seed): IO[UserMessage, Entity] + +/** A factory using idGenerator to create the entity */ +class GeneratingFactory[Entity, Seed, Id]( + constructor: Seed => Validated[Id => Entity], + idGenerator: IdGenerator[Id] +) extends EntityFactory[Entity, Seed]: + override def make(seed: Seed): IO[UserMessage, Entity] = + for + construct <- constructor(seed).toZIO + id <- idGenerator.nextId + yield construct(id) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala new file mode 100644 index 0000000..29ae384 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.auth.CurrentUser + +trait EntityUpdateService[Id, Command, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def update(id: Id, command: Command): Op[Unit] + +trait EntityCreateService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(initData: Init): Op[Id] + +trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(id: Id, initData: Init): Op[Id] + +trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] + extends EntityCreateService[Id, Init, Error] + with EntityUpdateService[Id, Command, Error] diff --git a/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala new file mode 100644 index 0000000..946011a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import zio.* + +trait ViewProcessor[Event]: + def process(event: Event): UIO[Unit] diff --git a/entity/shared/src/main/scala/works/iterative/event/Event.scala b/entity/shared/src/main/scala/works/iterative/event/Event.scala new file mode 100644 index 0000000..7907ac0 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/Event.scala @@ -0,0 +1,4 @@ +package works.iterative.event + +trait Event[A]: + def record: EventRecord diff --git a/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala new file mode 100644 index 0000000..490417a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala @@ -0,0 +1,15 @@ +package works.iterative.event + +import works.iterative.core.* +import zio.* + +/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ +final case class EventRecord(userHandle: UserHandle, timestamp: Moment): + val displayName: String = userHandle.displayName + +object EventRecord: + def apply(userHandle: UserHandle): UIO[EventRecord] = + Moment.now.map(EventRecord(userHandle, _)) + + def now(using userHandle: UserHandle): UIO[EventRecord] = + apply(userHandle) diff --git a/entity/shared/src/main/scala/works/iterative/event/EventStore.scala b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala new file mode 100644 index 0000000..3226df3 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala @@ -0,0 +1,13 @@ +package works.iterative.event + +import zio.* + +/** Abstraction of event log + * + * The log can persist events and restore the state of an entity from the + * events. + */ +trait EventStore[Id, T <: Event[_]]: + type Op[A] = UIO[A] + def persist(id: Id, event: T): Op[Unit] + def get(id: Id): Op[Seq[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala new file mode 100644 index 0000000..6ccccd7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala @@ -0,0 +1,12 @@ +package works.iterative.event + +import zio.* + +/** Snapshot store + * + * Stores a snapshot of an entity. + */ +trait SnapshotStore[Id, T]: + type Op[A] = UIO[A] + def update(id: Id, snapshot: T): Op[Unit] + def get(id: Id): Op[Option[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala new file mode 100644 index 0000000..964d420 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala @@ -0,0 +1,21 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) + extends EventStore[Id, T]: + override def persist(id: Id, event: T): Op[Unit] = + data.update { m => + val events = m.getOrElse(id, Nil) + m.updated(id, event :: events) + }.unit + override def get(id: Id): Op[Seq[T]] = + data.get.map(_.getOrElse(id, Nil)) + +object InMemoryEventStore: + def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, List[T]]) + yield InMemoryEventStore(data) + } diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala new file mode 100644 index 0000000..722d7d7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala @@ -0,0 +1,19 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) + extends SnapshotStore[Id, T]: + override def update(id: Id, snapshot: T): UIO[Unit] = + data.update(_.updated(id, snapshot)).unit + + override def get(id: Id): UIO[Option[T]] = + data.get.map(_.get(id)) + +object InMemorySnapshotStore: + def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, T]) + yield InMemorySnapshotStore(data) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala new file mode 100644 index 0000000..874d16c --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala @@ -0,0 +1,63 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import sttp.client3.* + +class ConsulKeyValueStore( + token: ConsulToken, + baseUri: BaseUri, + backend: CustomTapir.Backend +) extends KeyValueStore[String, String]: + + private val defaultBase = "http://localhost:8500" + private val base = baseUri.value.map(_.toString).getOrElse(defaultBase) + + private def addAuth[U[_], T, R]( + request: RequestT[U, T, R] + ): RequestT[U, T, R] = + request.header("X-Consul-Token", token.token) + + override def get(key: String): UIO[Option[String]] = { + for + response <- addAuth(basicRequest.get(uri"$base/v1/kv/${key}?raw=true")) + .send(backend) + yield response.body.toOption + }.orDie + + override def put(key: String, value: String): UIO[Unit] = { + addAuth(basicRequest.put(uri"$base/v1/kv/${key}").body(value)) + .send(backend) + .unit + }.orDie + + override def remove(key: String): UIO[Unit] = { + addAuth(basicRequest.delete(uri"$base/v1/kv/${key}")) + .send(backend) + .unit + }.orDie + +object ConsulKeyValueStore: + val layer: URLayer[ + ConsulToken & BaseUri & CustomTapir.Backend, + StringKeyValueStore + ] = + ZLayer { + for + token <- ZIO.service[ConsulToken] + baseUri <- ZIO.service[BaseUri] + backend <- ZIO.service[CustomTapir.Backend] + yield ConsulKeyValueStore(token, baseUri, backend) + } + + val fromEnv + : ZLayer[CustomTapir.Backend, SecurityException, StringKeyValueStore] = + ConsulToken.fromEnv >>> ZLayer { + for + token <- ZIO.service[ConsulToken] + backend <- ZIO.service[CustomTapir.Backend] + addr <- System.envOrElse("CONSUL_ADDR", "http://localhost:8500") + yield ConsulKeyValueStore(token, BaseUri(addr), backend) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala new file mode 100644 index 0000000..5c5483a --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala @@ -0,0 +1,150 @@ +package works.iterative.core.service.impl + +import zio.* +import sttp.tapir.EndpointIO.annotations.* +import sttp.tapir.Schema +import zio.json.* + +final case class ConsulToken(token: Option[String]) + +object ConsulToken: + def fromEnv: ZLayer[Any, SecurityException, ConsulToken] = ZLayer { + System.env("CONSUL_TOKEN").map(ConsulToken(_)) + } + +@jsonMemberNames(PascalCase) +final case class ConsulMetadata( + createIndex: Long, + modifyIndex: Long, + lockIndex: Long, + key: String, + flags: Long, + value: String, + session: Option[String] +) derives JsonCodec, + Schema + +@endpointInput("v1/kv/{key}") +final case class GetQuery( + @path + @description("Key to get from the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + // TODO: This would not work type-wise properly, as the result type of the call would differ depending on the query parameters + // These are missing, split to different queries to enable. + /* + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies the response is just the raw value of the key, without any encoding or metadata." + ) + raw: Option[Boolean] = None, + @query + @description( + "Specifies to return only keys (no values or metadata). Specifying this parameter implies recurse." + ) + keys: Option[Boolean] = None, + @query + @description( + "Specifies the string to use as a separator for recursive key lookups. This option is only used when paired with the keys parameter to limit the prefix of keys returned, only up to the given separator." + ) + separator: Option[String] = None, + */ + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class PutQuery( + @path + @description("Key to put into the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies an unsigned value between 0 and (2^64)-1 to store with the key. API consumers can use this field any way they choose for their application." + ) + flags: Option[Long] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Supply a session ID to use in a lock acquisition operation.") + acquire: Option[String] = None, + @query + @description("Supply a session ID to use in a release operation.") + release: Option[String] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class DeleteQuery( + @path + @description("Key to delete in the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +/* +sealed trait ConsulGetError +object ConsulGetError: + case object NotFound extends ConsulGetError derives JsonCodec, Schema + case object Forbidden extends ConsulGetError derives JsonCodec, Schema + case class ServerError(msg: String) extends ConsulGetError derives JsonCodec, Schema + +trait ConsulKVEndpoints extends CustomTapir: + private val kvBase = endpoint + .in("v1" / "kv") + .securityIn( + auth.apiKey(header[Option[String]]("X-Consul-Token").mapTo[ConsulToken]) + ) + + val getKV: Endpoint[ConsulToken, GetQuery, Unit, ConsulMetadata, Any] = + kvBase.get + .in(EndpointInput.derived[GetQuery]) + .out(jsonBody[ConsulMetadata]) + .errorOut( + oneOf[ConsulGetError]( + oneOfVariant( + statusCode(StatusCode.NotFound).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfVariant( + statusCode(StatusCode.Forbidden).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfDefaultVariant( + statusCode(StatusCode.InternalServerError).and(jsonBody[ServerError].description("not found")) + ) + ) + ) + val putKV: Endpoint[ConsulToken, PutQuery, Unit, Boolean, Any] = + kvBase.put.in(EndpointInput.derived[PutQuery]).out(jsonBody[Boolean]) + val deleteKV: Endpoint[ConsulToken, DeleteQuery, Unit, Boolean, Any] = + kvBase.delete.in(EndpointInput.derived[DeleteQuery]).out(jsonBody[Boolean]) + +*/ \ No newline at end of file diff --git a/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala new file mode 100644 index 0000000..c938083 --- /dev/null +++ b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala @@ -0,0 +1,19 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import zio.test.* + +object ConsulKeyValueStoreSpec extends specs.KeyValueStoreSpec: + override def spec = defaultSpec.provide( + ConsulKeyValueStore.layer, + CustomTapir.clientLayer, + ZLayer { + Live.live(System.env("CONSUL_ADDR")).map(a => BaseUri(a.get)) + }, + ZLayer { + Live.live(System.env("CONSUL_TOKEN")).map(ConsulToken(_)) + } + ) @@ TestAspect.ifEnvSet("CONSUL_ADDR") diff --git a/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala new file mode 100644 index 0000000..16211a8 --- /dev/null +++ b/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala @@ -0,0 +1,29 @@ +package works.iterative.core.service.impl + +import org.scalajs.dom.Storage +import works.iterative.core.service.Repository +import zio.* +import zio.json.* + +// TODO: improve error reporting on generic repositories +// This is good just for prototypes +// And it cannot be used to query data, as there is no way to iterate the storage +class JsStorageRepository[Value: JsonCodec]( + storage: Storage +) extends Repository[String, Value, String]: + + override def load(id: String): UIO[Option[Value]] = { + for + raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) + data <- ZIO.foreach(raw) { r => + ZIO + .fromEither(r.fromJson[Value]) + .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) + } + yield data + }.orDie + + override def save(key: String, value: Value): UIO[Unit] = + ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie + + override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala new file mode 100644 index 0000000..8c88818 --- /dev/null +++ b/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala @@ -0,0 +1,26 @@ +package works.iterative.core +package service +package impl + +import zio.* +import zio.stream.* + +class JcaDigestService extends DigestService: + + override def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): IO[E, Digest] = + for + md <- ZIO + .attempt( + java.security.MessageDigest.getInstance(algorithm.value) + ) + .orDie + _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) + digest <- ZIO.attempt(md.digest()).orDie + yield Digest(algorithm, digest) + +object JcaDigestGenerator: + val layer: ULayer[DigestService] = + ZLayer.succeed(new JcaDigestService) diff --git a/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala new file mode 100644 index 0000000..883bd7a --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala @@ -0,0 +1,26 @@ +package works.iterative.core.service + +import works.iterative.core.Digest +import works.iterative.core.DigestAlgorithm + +import zio.* +import zio.stream.* + +trait DigestService: + def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): IO[E, Digest] + +object DigestService: + def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): ZIO[DigestService, E, Digest] = + ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) + + def digest( + algorithm: DigestAlgorithm, + content: Array[Byte] + ): URIO[DigestService, Digest] = + digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala new file mode 100644 index 0000000..10bdeb7 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala @@ -0,0 +1,29 @@ +package works.iterative.core.service + +import works.iterative.core.FileSupport.* +import works.iterative.core.FileRef + +import zio.* + +trait FileStore: + type Op[A] = UIO[A] + def store(file: FileRepr): Op[FileRef] + def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] + def load(url: String): Op[Option[Array[Byte]]] + +object FileStore: + type Op[A] = URIO[FileStore, A] + def store(file: FileRepr): Op[FileRef] = + ZIO.serviceWithZIO(_.store(file)) + def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] = + ZIO.serviceWithZIO(_.store(name, file, contentType)) + def load(url: String): Op[Option[Array[Byte]]] = + ZIO.serviceWithZIO(_.load(url)) diff --git a/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala new file mode 100644 index 0000000..1779da3 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala @@ -0,0 +1,10 @@ +package works.iterative.core.service + +import zio.* + +/** Generator of unique IDs of a given type */ +trait IdGenerator[A]: + self => + def nextId: UIO[A] + def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: + def nextId: UIO[B] = self.nextId.map(f) diff --git a/service/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/service/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala new file mode 100644 index 0000000..23d1908 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala @@ -0,0 +1,47 @@ +package works.iterative.core.service + +import zio.* +import zio.json.* + +trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: + type Op[A] = Eff[A] + def get(key: Key): Eff[Option[Value]] + +trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: + def put(key: Key, value: Value): Eff[Unit] + def remove(key: Key): Eff[Unit] + +trait GenericKeyValueStore[Eff[+_], -Key, Value] + extends GenericReadKeyValueStore[Eff, Key, Value] + with GenericWriteKeyValueStore[Eff, Key, Value] + +trait ReadKeyValueStore[-Key, +Value] + extends GenericReadKeyValueStore[UIO, Key, Value] + +trait WriteKeyValueStore[-Key, -Value] + extends GenericWriteKeyValueStore[UIO, Key, Value] + +trait KeyValueStore[-Key, Value] + extends GenericKeyValueStore[UIO, Key, Value] + with ReadKeyValueStore[Key, Value] + with WriteKeyValueStore[Key, Value] + +type StringKeyValueStore = KeyValueStore[String, String] + +object KeyValueStore: + extension (store: StringKeyValueStore) + /** Decode the value, ignoring decoding errors if any */ + def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = + store.get(key).map(_.flatMap(_.fromJson[A].toOption)) + + def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = + store.get(key).flatMap { + case None => ZIO.none + case Some(v) => + ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => + ZIO.log(err) *> ZIO.none + } + } + + def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = + store.put(key, value.toJson) diff --git a/service/shared/src/main/scala/works/iterative/core/service/Repository.scala b/service/shared/src/main/scala/works/iterative/core/service/Repository.scala new file mode 100644 index 0000000..c450279 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/Repository.scala @@ -0,0 +1,52 @@ +package works.iterative.core.service + +import zio.* +import zio.stream.* + +trait GenericLoadService[Eff[+_], -Key, +Value]: + type Op[A] = Eff[A] + def load(id: Key): Op[Option[Value]] + +trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: + def updates: Eff[Str[Key]] + +trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: + type Op[A] = Eff[A] + def loadAll(ids: Seq[Key]): Op[Coll[Value]] + +trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: + type Op[A] = Eff[A] + def find(filter: FilterArg): Op[Coll[Value]] + +trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] + extends GenericLoadService[Eff, Key, Value] + with GenericLoadAllService[Eff, Coll, Key, Value] + with GenericFindService[Eff, Coll, Key, Value, FilterArg] + +trait GenericWriteRepository[Eff[_], -Key, -Value]: + type Op[A] = Eff[A] + def save(key: Key, value: Value): Op[Unit] + +trait GenericRepository[Eff[+_], -Key, Value] + extends GenericReadRepository[Eff, List, Key, Value, Unit] + with GenericWriteRepository[Eff, Key, Value] + +type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] +type LoadAllRepository[-Key, +Value] = + GenericLoadAllService[UIO, List, Key, Value] + +trait ReadRepository[-Key, +Value, -FilterArg] + extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: + override def loadAll(ids: Seq[Key]): UIO[List[Value]] = + // Inefficient implementation, meant to be overridden + ZIO.foreach(ids)(load).map(_.flatten.toList) + +trait UpdateNotifyRepository[Key] + extends GenericUpdateNotifyService[UIO, UStream, Key] + +trait WriteRepository[-Key, -Value] + extends GenericWriteRepository[UIO, Key, Value] + +trait Repository[-Key, Value, -FilterArg] + extends ReadRepository[Key, Value, FilterArg] + with WriteRepository[Key, Value] diff --git a/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala new file mode 100644 index 0000000..66ad66b --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala @@ -0,0 +1,21 @@ +package works.iterative.core.service +package impl + +import works.iterative.core.FileSupport.* +import works.iterative.core.FileRef +import zio.* + +object InMemoryFileStore: + val layer: ULayer[FileStore] = ZLayer.succeed { + new FileStore: + override def store(file: FileRepr): Op[FileRef] = + ZIO.succeed(FileRef.unsafe(file.name, "#")) + override def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] = + ZIO.succeed(FileRef.unsafe(name, "#")) + override def load(url: String): Op[Option[Array[Byte]]] = + ZIO.succeed(None) + } diff --git a/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala new file mode 100644 index 0000000..e3b7d45 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala @@ -0,0 +1,17 @@ +package works.iterative.core.service +package impl + +import zio.* + +trait InMemoryRepository[Key, Value, FilterArg]( + data: Ref[Map[Key, Value]], + filter: FilterArg => Value => Boolean +) extends Repository[Key, Value, FilterArg]: + override def save(key: Key, value: Value): UIO[Unit] = + data.update(_ + (key -> value)) + override def load(key: Key): UIO[Option[Value]] = + data.get.map(_.get(key)) + override def loadAll(keys: Seq[Key]): UIO[List[Value]] = + data.get.map(_.view.filterKeys(keys.contains).values.toList) + override def find(filterArg: FilterArg): UIO[List[Value]] = + data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/service/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/service/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala new file mode 100644 index 0000000..358cb9d --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala @@ -0,0 +1,11 @@ +package works.iterative.core.service.impl + +import works.iterative.core.service.IdGenerator +import zio.* + +class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: + def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) + +object UUIDGenerator: + def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = + ZLayer.succeed(UUIDGenerator(f)) diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala new file mode 100644 index 0000000..65c7e92 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* + +/** Represents aggregate root in DDD + * + * @tparam Id + * Aggregate root id + * @tparam Command + * Aggregate root command + * @tparam Event + * Aggregate root event + * @tparam State + * Aggregate root state + */ +trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: + /** Aggregate root id */ + def id: Id + + /** Current state */ + def state: UIO[State] + + def handle(command: Command): IO[Error, Unit] diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala new file mode 100644 index 0000000..c051650 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala @@ -0,0 +1,58 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator + +sealed trait FactoryError: + def entityId: String + def userMessage: UserMessage + +object FactoryError: + case class EntityAlreadyExists[Id](entityId: String, id: Id) + extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) + + case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) + +trait AggregateRootFactory[ + Ident, + T <: AggregateRoot[Ident, ?, ?, ?, ?] +]: + type Id = Ident + type NotFound = FactoryError.EntityNotFound[Id] + type AlreadyExists = FactoryError.EntityAlreadyExists[Id] + + protected def AlreadyExists(id: Id): AlreadyExists = + FactoryError.EntityAlreadyExists(entityId, id) + + protected def NotFound(id: Id): NotFound = + FactoryError.EntityNotFound(entityId, id) + + def entityId: String + def make(id: Id): IO[AlreadyExists, T] + def load(id: Id): IO[NotFound, T] + + def loadOrMake(id: Id): UIO[T] = + load(id) + .orElse(make(id)) + .orDieWith(_ => + new RuntimeException( + s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" + ) + ) + + def make()(using idGen: IdGenerator[Id]): UIO[T] = + val tryToCreate = for + id <- idGen.nextId + entity <- make(id) + yield entity + + tryToCreate + .retryN(10) + .orDieWith(_ => + new RuntimeException( + "Cannot create entity, generator failed 10 times to create new ID" + ) + ) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala new file mode 100644 index 0000000..b2fef3a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala @@ -0,0 +1,24 @@ +package works.iterative.entity + +import works.iterative.core.Validated +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator +import zio.* + +/** Generic class for entity factories. + * + * Creates a new entity from a "seed". + */ +trait EntityFactory[Entity, Seed]: + def make(seed: Seed): IO[UserMessage, Entity] + +/** A factory using idGenerator to create the entity */ +class GeneratingFactory[Entity, Seed, Id]( + constructor: Seed => Validated[Id => Entity], + idGenerator: IdGenerator[Id] +) extends EntityFactory[Entity, Seed]: + override def make(seed: Seed): IO[UserMessage, Entity] = + for + construct <- constructor(seed).toZIO + id <- idGenerator.nextId + yield construct(id) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala new file mode 100644 index 0000000..29ae384 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.auth.CurrentUser + +trait EntityUpdateService[Id, Command, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def update(id: Id, command: Command): Op[Unit] + +trait EntityCreateService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(initData: Init): Op[Id] + +trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(id: Id, initData: Init): Op[Id] + +trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] + extends EntityCreateService[Id, Init, Error] + with EntityUpdateService[Id, Command, Error] diff --git a/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala new file mode 100644 index 0000000..946011a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import zio.* + +trait ViewProcessor[Event]: + def process(event: Event): UIO[Unit] diff --git a/entity/shared/src/main/scala/works/iterative/event/Event.scala b/entity/shared/src/main/scala/works/iterative/event/Event.scala new file mode 100644 index 0000000..7907ac0 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/Event.scala @@ -0,0 +1,4 @@ +package works.iterative.event + +trait Event[A]: + def record: EventRecord diff --git a/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala new file mode 100644 index 0000000..490417a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala @@ -0,0 +1,15 @@ +package works.iterative.event + +import works.iterative.core.* +import zio.* + +/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ +final case class EventRecord(userHandle: UserHandle, timestamp: Moment): + val displayName: String = userHandle.displayName + +object EventRecord: + def apply(userHandle: UserHandle): UIO[EventRecord] = + Moment.now.map(EventRecord(userHandle, _)) + + def now(using userHandle: UserHandle): UIO[EventRecord] = + apply(userHandle) diff --git a/entity/shared/src/main/scala/works/iterative/event/EventStore.scala b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala new file mode 100644 index 0000000..3226df3 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala @@ -0,0 +1,13 @@ +package works.iterative.event + +import zio.* + +/** Abstraction of event log + * + * The log can persist events and restore the state of an entity from the + * events. + */ +trait EventStore[Id, T <: Event[_]]: + type Op[A] = UIO[A] + def persist(id: Id, event: T): Op[Unit] + def get(id: Id): Op[Seq[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala new file mode 100644 index 0000000..6ccccd7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala @@ -0,0 +1,12 @@ +package works.iterative.event + +import zio.* + +/** Snapshot store + * + * Stores a snapshot of an entity. + */ +trait SnapshotStore[Id, T]: + type Op[A] = UIO[A] + def update(id: Id, snapshot: T): Op[Unit] + def get(id: Id): Op[Option[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala new file mode 100644 index 0000000..964d420 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala @@ -0,0 +1,21 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) + extends EventStore[Id, T]: + override def persist(id: Id, event: T): Op[Unit] = + data.update { m => + val events = m.getOrElse(id, Nil) + m.updated(id, event :: events) + }.unit + override def get(id: Id): Op[Seq[T]] = + data.get.map(_.getOrElse(id, Nil)) + +object InMemoryEventStore: + def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, List[T]]) + yield InMemoryEventStore(data) + } diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala new file mode 100644 index 0000000..722d7d7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala @@ -0,0 +1,19 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) + extends SnapshotStore[Id, T]: + override def update(id: Id, snapshot: T): UIO[Unit] = + data.update(_.updated(id, snapshot)).unit + + override def get(id: Id): UIO[Option[T]] = + data.get.map(_.get(id)) + +object InMemorySnapshotStore: + def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, T]) + yield InMemorySnapshotStore(data) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala new file mode 100644 index 0000000..874d16c --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala @@ -0,0 +1,63 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import sttp.client3.* + +class ConsulKeyValueStore( + token: ConsulToken, + baseUri: BaseUri, + backend: CustomTapir.Backend +) extends KeyValueStore[String, String]: + + private val defaultBase = "http://localhost:8500" + private val base = baseUri.value.map(_.toString).getOrElse(defaultBase) + + private def addAuth[U[_], T, R]( + request: RequestT[U, T, R] + ): RequestT[U, T, R] = + request.header("X-Consul-Token", token.token) + + override def get(key: String): UIO[Option[String]] = { + for + response <- addAuth(basicRequest.get(uri"$base/v1/kv/${key}?raw=true")) + .send(backend) + yield response.body.toOption + }.orDie + + override def put(key: String, value: String): UIO[Unit] = { + addAuth(basicRequest.put(uri"$base/v1/kv/${key}").body(value)) + .send(backend) + .unit + }.orDie + + override def remove(key: String): UIO[Unit] = { + addAuth(basicRequest.delete(uri"$base/v1/kv/${key}")) + .send(backend) + .unit + }.orDie + +object ConsulKeyValueStore: + val layer: URLayer[ + ConsulToken & BaseUri & CustomTapir.Backend, + StringKeyValueStore + ] = + ZLayer { + for + token <- ZIO.service[ConsulToken] + baseUri <- ZIO.service[BaseUri] + backend <- ZIO.service[CustomTapir.Backend] + yield ConsulKeyValueStore(token, baseUri, backend) + } + + val fromEnv + : ZLayer[CustomTapir.Backend, SecurityException, StringKeyValueStore] = + ConsulToken.fromEnv >>> ZLayer { + for + token <- ZIO.service[ConsulToken] + backend <- ZIO.service[CustomTapir.Backend] + addr <- System.envOrElse("CONSUL_ADDR", "http://localhost:8500") + yield ConsulKeyValueStore(token, BaseUri(addr), backend) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala new file mode 100644 index 0000000..5c5483a --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala @@ -0,0 +1,150 @@ +package works.iterative.core.service.impl + +import zio.* +import sttp.tapir.EndpointIO.annotations.* +import sttp.tapir.Schema +import zio.json.* + +final case class ConsulToken(token: Option[String]) + +object ConsulToken: + def fromEnv: ZLayer[Any, SecurityException, ConsulToken] = ZLayer { + System.env("CONSUL_TOKEN").map(ConsulToken(_)) + } + +@jsonMemberNames(PascalCase) +final case class ConsulMetadata( + createIndex: Long, + modifyIndex: Long, + lockIndex: Long, + key: String, + flags: Long, + value: String, + session: Option[String] +) derives JsonCodec, + Schema + +@endpointInput("v1/kv/{key}") +final case class GetQuery( + @path + @description("Key to get from the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + // TODO: This would not work type-wise properly, as the result type of the call would differ depending on the query parameters + // These are missing, split to different queries to enable. + /* + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies the response is just the raw value of the key, without any encoding or metadata." + ) + raw: Option[Boolean] = None, + @query + @description( + "Specifies to return only keys (no values or metadata). Specifying this parameter implies recurse." + ) + keys: Option[Boolean] = None, + @query + @description( + "Specifies the string to use as a separator for recursive key lookups. This option is only used when paired with the keys parameter to limit the prefix of keys returned, only up to the given separator." + ) + separator: Option[String] = None, + */ + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class PutQuery( + @path + @description("Key to put into the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies an unsigned value between 0 and (2^64)-1 to store with the key. API consumers can use this field any way they choose for their application." + ) + flags: Option[Long] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Supply a session ID to use in a lock acquisition operation.") + acquire: Option[String] = None, + @query + @description("Supply a session ID to use in a release operation.") + release: Option[String] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class DeleteQuery( + @path + @description("Key to delete in the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +/* +sealed trait ConsulGetError +object ConsulGetError: + case object NotFound extends ConsulGetError derives JsonCodec, Schema + case object Forbidden extends ConsulGetError derives JsonCodec, Schema + case class ServerError(msg: String) extends ConsulGetError derives JsonCodec, Schema + +trait ConsulKVEndpoints extends CustomTapir: + private val kvBase = endpoint + .in("v1" / "kv") + .securityIn( + auth.apiKey(header[Option[String]]("X-Consul-Token").mapTo[ConsulToken]) + ) + + val getKV: Endpoint[ConsulToken, GetQuery, Unit, ConsulMetadata, Any] = + kvBase.get + .in(EndpointInput.derived[GetQuery]) + .out(jsonBody[ConsulMetadata]) + .errorOut( + oneOf[ConsulGetError]( + oneOfVariant( + statusCode(StatusCode.NotFound).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfVariant( + statusCode(StatusCode.Forbidden).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfDefaultVariant( + statusCode(StatusCode.InternalServerError).and(jsonBody[ServerError].description("not found")) + ) + ) + ) + val putKV: Endpoint[ConsulToken, PutQuery, Unit, Boolean, Any] = + kvBase.put.in(EndpointInput.derived[PutQuery]).out(jsonBody[Boolean]) + val deleteKV: Endpoint[ConsulToken, DeleteQuery, Unit, Boolean, Any] = + kvBase.delete.in(EndpointInput.derived[DeleteQuery]).out(jsonBody[Boolean]) + +*/ \ No newline at end of file diff --git a/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala new file mode 100644 index 0000000..c938083 --- /dev/null +++ b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala @@ -0,0 +1,19 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import zio.test.* + +object ConsulKeyValueStoreSpec extends specs.KeyValueStoreSpec: + override def spec = defaultSpec.provide( + ConsulKeyValueStore.layer, + CustomTapir.clientLayer, + ZLayer { + Live.live(System.env("CONSUL_ADDR")).map(a => BaseUri(a.get)) + }, + ZLayer { + Live.live(System.env("CONSUL_TOKEN")).map(ConsulToken(_)) + } + ) @@ TestAspect.ifEnvSet("CONSUL_ADDR") diff --git a/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala new file mode 100644 index 0000000..16211a8 --- /dev/null +++ b/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala @@ -0,0 +1,29 @@ +package works.iterative.core.service.impl + +import org.scalajs.dom.Storage +import works.iterative.core.service.Repository +import zio.* +import zio.json.* + +// TODO: improve error reporting on generic repositories +// This is good just for prototypes +// And it cannot be used to query data, as there is no way to iterate the storage +class JsStorageRepository[Value: JsonCodec]( + storage: Storage +) extends Repository[String, Value, String]: + + override def load(id: String): UIO[Option[Value]] = { + for + raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) + data <- ZIO.foreach(raw) { r => + ZIO + .fromEither(r.fromJson[Value]) + .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) + } + yield data + }.orDie + + override def save(key: String, value: Value): UIO[Unit] = + ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie + + override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala new file mode 100644 index 0000000..8c88818 --- /dev/null +++ b/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala @@ -0,0 +1,26 @@ +package works.iterative.core +package service +package impl + +import zio.* +import zio.stream.* + +class JcaDigestService extends DigestService: + + override def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): IO[E, Digest] = + for + md <- ZIO + .attempt( + java.security.MessageDigest.getInstance(algorithm.value) + ) + .orDie + _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) + digest <- ZIO.attempt(md.digest()).orDie + yield Digest(algorithm, digest) + +object JcaDigestGenerator: + val layer: ULayer[DigestService] = + ZLayer.succeed(new JcaDigestService) diff --git a/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala new file mode 100644 index 0000000..883bd7a --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala @@ -0,0 +1,26 @@ +package works.iterative.core.service + +import works.iterative.core.Digest +import works.iterative.core.DigestAlgorithm + +import zio.* +import zio.stream.* + +trait DigestService: + def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): IO[E, Digest] + +object DigestService: + def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): ZIO[DigestService, E, Digest] = + ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) + + def digest( + algorithm: DigestAlgorithm, + content: Array[Byte] + ): URIO[DigestService, Digest] = + digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala new file mode 100644 index 0000000..10bdeb7 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala @@ -0,0 +1,29 @@ +package works.iterative.core.service + +import works.iterative.core.FileSupport.* +import works.iterative.core.FileRef + +import zio.* + +trait FileStore: + type Op[A] = UIO[A] + def store(file: FileRepr): Op[FileRef] + def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] + def load(url: String): Op[Option[Array[Byte]]] + +object FileStore: + type Op[A] = URIO[FileStore, A] + def store(file: FileRepr): Op[FileRef] = + ZIO.serviceWithZIO(_.store(file)) + def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] = + ZIO.serviceWithZIO(_.store(name, file, contentType)) + def load(url: String): Op[Option[Array[Byte]]] = + ZIO.serviceWithZIO(_.load(url)) diff --git a/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala new file mode 100644 index 0000000..1779da3 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala @@ -0,0 +1,10 @@ +package works.iterative.core.service + +import zio.* + +/** Generator of unique IDs of a given type */ +trait IdGenerator[A]: + self => + def nextId: UIO[A] + def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: + def nextId: UIO[B] = self.nextId.map(f) diff --git a/service/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/service/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala new file mode 100644 index 0000000..23d1908 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala @@ -0,0 +1,47 @@ +package works.iterative.core.service + +import zio.* +import zio.json.* + +trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: + type Op[A] = Eff[A] + def get(key: Key): Eff[Option[Value]] + +trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: + def put(key: Key, value: Value): Eff[Unit] + def remove(key: Key): Eff[Unit] + +trait GenericKeyValueStore[Eff[+_], -Key, Value] + extends GenericReadKeyValueStore[Eff, Key, Value] + with GenericWriteKeyValueStore[Eff, Key, Value] + +trait ReadKeyValueStore[-Key, +Value] + extends GenericReadKeyValueStore[UIO, Key, Value] + +trait WriteKeyValueStore[-Key, -Value] + extends GenericWriteKeyValueStore[UIO, Key, Value] + +trait KeyValueStore[-Key, Value] + extends GenericKeyValueStore[UIO, Key, Value] + with ReadKeyValueStore[Key, Value] + with WriteKeyValueStore[Key, Value] + +type StringKeyValueStore = KeyValueStore[String, String] + +object KeyValueStore: + extension (store: StringKeyValueStore) + /** Decode the value, ignoring decoding errors if any */ + def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = + store.get(key).map(_.flatMap(_.fromJson[A].toOption)) + + def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = + store.get(key).flatMap { + case None => ZIO.none + case Some(v) => + ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => + ZIO.log(err) *> ZIO.none + } + } + + def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = + store.put(key, value.toJson) diff --git a/service/shared/src/main/scala/works/iterative/core/service/Repository.scala b/service/shared/src/main/scala/works/iterative/core/service/Repository.scala new file mode 100644 index 0000000..c450279 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/Repository.scala @@ -0,0 +1,52 @@ +package works.iterative.core.service + +import zio.* +import zio.stream.* + +trait GenericLoadService[Eff[+_], -Key, +Value]: + type Op[A] = Eff[A] + def load(id: Key): Op[Option[Value]] + +trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: + def updates: Eff[Str[Key]] + +trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: + type Op[A] = Eff[A] + def loadAll(ids: Seq[Key]): Op[Coll[Value]] + +trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: + type Op[A] = Eff[A] + def find(filter: FilterArg): Op[Coll[Value]] + +trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] + extends GenericLoadService[Eff, Key, Value] + with GenericLoadAllService[Eff, Coll, Key, Value] + with GenericFindService[Eff, Coll, Key, Value, FilterArg] + +trait GenericWriteRepository[Eff[_], -Key, -Value]: + type Op[A] = Eff[A] + def save(key: Key, value: Value): Op[Unit] + +trait GenericRepository[Eff[+_], -Key, Value] + extends GenericReadRepository[Eff, List, Key, Value, Unit] + with GenericWriteRepository[Eff, Key, Value] + +type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] +type LoadAllRepository[-Key, +Value] = + GenericLoadAllService[UIO, List, Key, Value] + +trait ReadRepository[-Key, +Value, -FilterArg] + extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: + override def loadAll(ids: Seq[Key]): UIO[List[Value]] = + // Inefficient implementation, meant to be overridden + ZIO.foreach(ids)(load).map(_.flatten.toList) + +trait UpdateNotifyRepository[Key] + extends GenericUpdateNotifyService[UIO, UStream, Key] + +trait WriteRepository[-Key, -Value] + extends GenericWriteRepository[UIO, Key, Value] + +trait Repository[-Key, Value, -FilterArg] + extends ReadRepository[Key, Value, FilterArg] + with WriteRepository[Key, Value] diff --git a/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala new file mode 100644 index 0000000..66ad66b --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala @@ -0,0 +1,21 @@ +package works.iterative.core.service +package impl + +import works.iterative.core.FileSupport.* +import works.iterative.core.FileRef +import zio.* + +object InMemoryFileStore: + val layer: ULayer[FileStore] = ZLayer.succeed { + new FileStore: + override def store(file: FileRepr): Op[FileRef] = + ZIO.succeed(FileRef.unsafe(file.name, "#")) + override def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] = + ZIO.succeed(FileRef.unsafe(name, "#")) + override def load(url: String): Op[Option[Array[Byte]]] = + ZIO.succeed(None) + } diff --git a/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala new file mode 100644 index 0000000..e3b7d45 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala @@ -0,0 +1,17 @@ +package works.iterative.core.service +package impl + +import zio.* + +trait InMemoryRepository[Key, Value, FilterArg]( + data: Ref[Map[Key, Value]], + filter: FilterArg => Value => Boolean +) extends Repository[Key, Value, FilterArg]: + override def save(key: Key, value: Value): UIO[Unit] = + data.update(_ + (key -> value)) + override def load(key: Key): UIO[Option[Value]] = + data.get.map(_.get(key)) + override def loadAll(keys: Seq[Key]): UIO[List[Value]] = + data.get.map(_.view.filterKeys(keys.contains).values.toList) + override def find(filterArg: FilterArg): UIO[List[Value]] = + data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/service/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/service/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala new file mode 100644 index 0000000..358cb9d --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala @@ -0,0 +1,11 @@ +package works.iterative.core.service.impl + +import works.iterative.core.service.IdGenerator +import zio.* + +class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: + def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) + +object UUIDGenerator: + def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = + ZLayer.succeed(UUIDGenerator(f)) diff --git a/service/specs/shared/src/main/scala/works/iterative/core/service/specs/KeyValueStoreSpec.scala b/service/specs/shared/src/main/scala/works/iterative/core/service/specs/KeyValueStoreSpec.scala new file mode 100644 index 0000000..0548a3f --- /dev/null +++ b/service/specs/shared/src/main/scala/works/iterative/core/service/specs/KeyValueStoreSpec.scala @@ -0,0 +1,34 @@ +package works.iterative.core +package service +package specs + +import zio.* +import zio.test.* + +abstract class KeyValueStoreSpec extends ZIOSpecDefault: + val defaultSpec = suite("Consul spec")( + test("get returns None if key does not exist") { + for + kv <- ZIO.service[StringKeyValueStore] + uuid <- Random.nextUUID + key = s"test/$uuid" + value <- kv.get(key) + yield assertTrue(value.isEmpty) + }, + test("read/put/delete works") { + for + kv <- ZIO.service[StringKeyValueStore] + uuid <- Random.nextUUID + key = s"test/$uuid" + originalValue <- kv.get(key) + _ <- kv.put(key, "test_value") + updatedValue <- kv.get(key) + _ <- kv.remove(key) + removedValue <- kv.get(key) + yield assertTrue( + originalValue.isEmpty, + updatedValue.contains("test_value"), + removedValue.isEmpty + ) + } + ) diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala new file mode 100644 index 0000000..65c7e92 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* + +/** Represents aggregate root in DDD + * + * @tparam Id + * Aggregate root id + * @tparam Command + * Aggregate root command + * @tparam Event + * Aggregate root event + * @tparam State + * Aggregate root state + */ +trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: + /** Aggregate root id */ + def id: Id + + /** Current state */ + def state: UIO[State] + + def handle(command: Command): IO[Error, Unit] diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala new file mode 100644 index 0000000..c051650 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala @@ -0,0 +1,58 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator + +sealed trait FactoryError: + def entityId: String + def userMessage: UserMessage + +object FactoryError: + case class EntityAlreadyExists[Id](entityId: String, id: Id) + extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) + + case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) + +trait AggregateRootFactory[ + Ident, + T <: AggregateRoot[Ident, ?, ?, ?, ?] +]: + type Id = Ident + type NotFound = FactoryError.EntityNotFound[Id] + type AlreadyExists = FactoryError.EntityAlreadyExists[Id] + + protected def AlreadyExists(id: Id): AlreadyExists = + FactoryError.EntityAlreadyExists(entityId, id) + + protected def NotFound(id: Id): NotFound = + FactoryError.EntityNotFound(entityId, id) + + def entityId: String + def make(id: Id): IO[AlreadyExists, T] + def load(id: Id): IO[NotFound, T] + + def loadOrMake(id: Id): UIO[T] = + load(id) + .orElse(make(id)) + .orDieWith(_ => + new RuntimeException( + s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" + ) + ) + + def make()(using idGen: IdGenerator[Id]): UIO[T] = + val tryToCreate = for + id <- idGen.nextId + entity <- make(id) + yield entity + + tryToCreate + .retryN(10) + .orDieWith(_ => + new RuntimeException( + "Cannot create entity, generator failed 10 times to create new ID" + ) + ) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala new file mode 100644 index 0000000..b2fef3a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala @@ -0,0 +1,24 @@ +package works.iterative.entity + +import works.iterative.core.Validated +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator +import zio.* + +/** Generic class for entity factories. + * + * Creates a new entity from a "seed". + */ +trait EntityFactory[Entity, Seed]: + def make(seed: Seed): IO[UserMessage, Entity] + +/** A factory using idGenerator to create the entity */ +class GeneratingFactory[Entity, Seed, Id]( + constructor: Seed => Validated[Id => Entity], + idGenerator: IdGenerator[Id] +) extends EntityFactory[Entity, Seed]: + override def make(seed: Seed): IO[UserMessage, Entity] = + for + construct <- constructor(seed).toZIO + id <- idGenerator.nextId + yield construct(id) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala new file mode 100644 index 0000000..29ae384 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.auth.CurrentUser + +trait EntityUpdateService[Id, Command, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def update(id: Id, command: Command): Op[Unit] + +trait EntityCreateService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(initData: Init): Op[Id] + +trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(id: Id, initData: Init): Op[Id] + +trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] + extends EntityCreateService[Id, Init, Error] + with EntityUpdateService[Id, Command, Error] diff --git a/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala new file mode 100644 index 0000000..946011a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import zio.* + +trait ViewProcessor[Event]: + def process(event: Event): UIO[Unit] diff --git a/entity/shared/src/main/scala/works/iterative/event/Event.scala b/entity/shared/src/main/scala/works/iterative/event/Event.scala new file mode 100644 index 0000000..7907ac0 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/Event.scala @@ -0,0 +1,4 @@ +package works.iterative.event + +trait Event[A]: + def record: EventRecord diff --git a/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala new file mode 100644 index 0000000..490417a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala @@ -0,0 +1,15 @@ +package works.iterative.event + +import works.iterative.core.* +import zio.* + +/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ +final case class EventRecord(userHandle: UserHandle, timestamp: Moment): + val displayName: String = userHandle.displayName + +object EventRecord: + def apply(userHandle: UserHandle): UIO[EventRecord] = + Moment.now.map(EventRecord(userHandle, _)) + + def now(using userHandle: UserHandle): UIO[EventRecord] = + apply(userHandle) diff --git a/entity/shared/src/main/scala/works/iterative/event/EventStore.scala b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala new file mode 100644 index 0000000..3226df3 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala @@ -0,0 +1,13 @@ +package works.iterative.event + +import zio.* + +/** Abstraction of event log + * + * The log can persist events and restore the state of an entity from the + * events. + */ +trait EventStore[Id, T <: Event[_]]: + type Op[A] = UIO[A] + def persist(id: Id, event: T): Op[Unit] + def get(id: Id): Op[Seq[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala new file mode 100644 index 0000000..6ccccd7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala @@ -0,0 +1,12 @@ +package works.iterative.event + +import zio.* + +/** Snapshot store + * + * Stores a snapshot of an entity. + */ +trait SnapshotStore[Id, T]: + type Op[A] = UIO[A] + def update(id: Id, snapshot: T): Op[Unit] + def get(id: Id): Op[Option[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala new file mode 100644 index 0000000..964d420 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala @@ -0,0 +1,21 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) + extends EventStore[Id, T]: + override def persist(id: Id, event: T): Op[Unit] = + data.update { m => + val events = m.getOrElse(id, Nil) + m.updated(id, event :: events) + }.unit + override def get(id: Id): Op[Seq[T]] = + data.get.map(_.getOrElse(id, Nil)) + +object InMemoryEventStore: + def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, List[T]]) + yield InMemoryEventStore(data) + } diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala new file mode 100644 index 0000000..722d7d7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala @@ -0,0 +1,19 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) + extends SnapshotStore[Id, T]: + override def update(id: Id, snapshot: T): UIO[Unit] = + data.update(_.updated(id, snapshot)).unit + + override def get(id: Id): UIO[Option[T]] = + data.get.map(_.get(id)) + +object InMemorySnapshotStore: + def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, T]) + yield InMemorySnapshotStore(data) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala new file mode 100644 index 0000000..874d16c --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala @@ -0,0 +1,63 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import sttp.client3.* + +class ConsulKeyValueStore( + token: ConsulToken, + baseUri: BaseUri, + backend: CustomTapir.Backend +) extends KeyValueStore[String, String]: + + private val defaultBase = "http://localhost:8500" + private val base = baseUri.value.map(_.toString).getOrElse(defaultBase) + + private def addAuth[U[_], T, R]( + request: RequestT[U, T, R] + ): RequestT[U, T, R] = + request.header("X-Consul-Token", token.token) + + override def get(key: String): UIO[Option[String]] = { + for + response <- addAuth(basicRequest.get(uri"$base/v1/kv/${key}?raw=true")) + .send(backend) + yield response.body.toOption + }.orDie + + override def put(key: String, value: String): UIO[Unit] = { + addAuth(basicRequest.put(uri"$base/v1/kv/${key}").body(value)) + .send(backend) + .unit + }.orDie + + override def remove(key: String): UIO[Unit] = { + addAuth(basicRequest.delete(uri"$base/v1/kv/${key}")) + .send(backend) + .unit + }.orDie + +object ConsulKeyValueStore: + val layer: URLayer[ + ConsulToken & BaseUri & CustomTapir.Backend, + StringKeyValueStore + ] = + ZLayer { + for + token <- ZIO.service[ConsulToken] + baseUri <- ZIO.service[BaseUri] + backend <- ZIO.service[CustomTapir.Backend] + yield ConsulKeyValueStore(token, baseUri, backend) + } + + val fromEnv + : ZLayer[CustomTapir.Backend, SecurityException, StringKeyValueStore] = + ConsulToken.fromEnv >>> ZLayer { + for + token <- ZIO.service[ConsulToken] + backend <- ZIO.service[CustomTapir.Backend] + addr <- System.envOrElse("CONSUL_ADDR", "http://localhost:8500") + yield ConsulKeyValueStore(token, BaseUri(addr), backend) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala new file mode 100644 index 0000000..5c5483a --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala @@ -0,0 +1,150 @@ +package works.iterative.core.service.impl + +import zio.* +import sttp.tapir.EndpointIO.annotations.* +import sttp.tapir.Schema +import zio.json.* + +final case class ConsulToken(token: Option[String]) + +object ConsulToken: + def fromEnv: ZLayer[Any, SecurityException, ConsulToken] = ZLayer { + System.env("CONSUL_TOKEN").map(ConsulToken(_)) + } + +@jsonMemberNames(PascalCase) +final case class ConsulMetadata( + createIndex: Long, + modifyIndex: Long, + lockIndex: Long, + key: String, + flags: Long, + value: String, + session: Option[String] +) derives JsonCodec, + Schema + +@endpointInput("v1/kv/{key}") +final case class GetQuery( + @path + @description("Key to get from the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + // TODO: This would not work type-wise properly, as the result type of the call would differ depending on the query parameters + // These are missing, split to different queries to enable. + /* + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies the response is just the raw value of the key, without any encoding or metadata." + ) + raw: Option[Boolean] = None, + @query + @description( + "Specifies to return only keys (no values or metadata). Specifying this parameter implies recurse." + ) + keys: Option[Boolean] = None, + @query + @description( + "Specifies the string to use as a separator for recursive key lookups. This option is only used when paired with the keys parameter to limit the prefix of keys returned, only up to the given separator." + ) + separator: Option[String] = None, + */ + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class PutQuery( + @path + @description("Key to put into the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies an unsigned value between 0 and (2^64)-1 to store with the key. API consumers can use this field any way they choose for their application." + ) + flags: Option[Long] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Supply a session ID to use in a lock acquisition operation.") + acquire: Option[String] = None, + @query + @description("Supply a session ID to use in a release operation.") + release: Option[String] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class DeleteQuery( + @path + @description("Key to delete in the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +/* +sealed trait ConsulGetError +object ConsulGetError: + case object NotFound extends ConsulGetError derives JsonCodec, Schema + case object Forbidden extends ConsulGetError derives JsonCodec, Schema + case class ServerError(msg: String) extends ConsulGetError derives JsonCodec, Schema + +trait ConsulKVEndpoints extends CustomTapir: + private val kvBase = endpoint + .in("v1" / "kv") + .securityIn( + auth.apiKey(header[Option[String]]("X-Consul-Token").mapTo[ConsulToken]) + ) + + val getKV: Endpoint[ConsulToken, GetQuery, Unit, ConsulMetadata, Any] = + kvBase.get + .in(EndpointInput.derived[GetQuery]) + .out(jsonBody[ConsulMetadata]) + .errorOut( + oneOf[ConsulGetError]( + oneOfVariant( + statusCode(StatusCode.NotFound).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfVariant( + statusCode(StatusCode.Forbidden).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfDefaultVariant( + statusCode(StatusCode.InternalServerError).and(jsonBody[ServerError].description("not found")) + ) + ) + ) + val putKV: Endpoint[ConsulToken, PutQuery, Unit, Boolean, Any] = + kvBase.put.in(EndpointInput.derived[PutQuery]).out(jsonBody[Boolean]) + val deleteKV: Endpoint[ConsulToken, DeleteQuery, Unit, Boolean, Any] = + kvBase.delete.in(EndpointInput.derived[DeleteQuery]).out(jsonBody[Boolean]) + +*/ \ No newline at end of file diff --git a/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala new file mode 100644 index 0000000..c938083 --- /dev/null +++ b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala @@ -0,0 +1,19 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import zio.test.* + +object ConsulKeyValueStoreSpec extends specs.KeyValueStoreSpec: + override def spec = defaultSpec.provide( + ConsulKeyValueStore.layer, + CustomTapir.clientLayer, + ZLayer { + Live.live(System.env("CONSUL_ADDR")).map(a => BaseUri(a.get)) + }, + ZLayer { + Live.live(System.env("CONSUL_TOKEN")).map(ConsulToken(_)) + } + ) @@ TestAspect.ifEnvSet("CONSUL_ADDR") diff --git a/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala new file mode 100644 index 0000000..16211a8 --- /dev/null +++ b/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala @@ -0,0 +1,29 @@ +package works.iterative.core.service.impl + +import org.scalajs.dom.Storage +import works.iterative.core.service.Repository +import zio.* +import zio.json.* + +// TODO: improve error reporting on generic repositories +// This is good just for prototypes +// And it cannot be used to query data, as there is no way to iterate the storage +class JsStorageRepository[Value: JsonCodec]( + storage: Storage +) extends Repository[String, Value, String]: + + override def load(id: String): UIO[Option[Value]] = { + for + raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) + data <- ZIO.foreach(raw) { r => + ZIO + .fromEither(r.fromJson[Value]) + .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) + } + yield data + }.orDie + + override def save(key: String, value: Value): UIO[Unit] = + ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie + + override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala new file mode 100644 index 0000000..8c88818 --- /dev/null +++ b/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala @@ -0,0 +1,26 @@ +package works.iterative.core +package service +package impl + +import zio.* +import zio.stream.* + +class JcaDigestService extends DigestService: + + override def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): IO[E, Digest] = + for + md <- ZIO + .attempt( + java.security.MessageDigest.getInstance(algorithm.value) + ) + .orDie + _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) + digest <- ZIO.attempt(md.digest()).orDie + yield Digest(algorithm, digest) + +object JcaDigestGenerator: + val layer: ULayer[DigestService] = + ZLayer.succeed(new JcaDigestService) diff --git a/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala new file mode 100644 index 0000000..883bd7a --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala @@ -0,0 +1,26 @@ +package works.iterative.core.service + +import works.iterative.core.Digest +import works.iterative.core.DigestAlgorithm + +import zio.* +import zio.stream.* + +trait DigestService: + def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): IO[E, Digest] + +object DigestService: + def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): ZIO[DigestService, E, Digest] = + ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) + + def digest( + algorithm: DigestAlgorithm, + content: Array[Byte] + ): URIO[DigestService, Digest] = + digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala new file mode 100644 index 0000000..10bdeb7 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala @@ -0,0 +1,29 @@ +package works.iterative.core.service + +import works.iterative.core.FileSupport.* +import works.iterative.core.FileRef + +import zio.* + +trait FileStore: + type Op[A] = UIO[A] + def store(file: FileRepr): Op[FileRef] + def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] + def load(url: String): Op[Option[Array[Byte]]] + +object FileStore: + type Op[A] = URIO[FileStore, A] + def store(file: FileRepr): Op[FileRef] = + ZIO.serviceWithZIO(_.store(file)) + def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] = + ZIO.serviceWithZIO(_.store(name, file, contentType)) + def load(url: String): Op[Option[Array[Byte]]] = + ZIO.serviceWithZIO(_.load(url)) diff --git a/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala new file mode 100644 index 0000000..1779da3 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala @@ -0,0 +1,10 @@ +package works.iterative.core.service + +import zio.* + +/** Generator of unique IDs of a given type */ +trait IdGenerator[A]: + self => + def nextId: UIO[A] + def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: + def nextId: UIO[B] = self.nextId.map(f) diff --git a/service/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/service/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala new file mode 100644 index 0000000..23d1908 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala @@ -0,0 +1,47 @@ +package works.iterative.core.service + +import zio.* +import zio.json.* + +trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: + type Op[A] = Eff[A] + def get(key: Key): Eff[Option[Value]] + +trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: + def put(key: Key, value: Value): Eff[Unit] + def remove(key: Key): Eff[Unit] + +trait GenericKeyValueStore[Eff[+_], -Key, Value] + extends GenericReadKeyValueStore[Eff, Key, Value] + with GenericWriteKeyValueStore[Eff, Key, Value] + +trait ReadKeyValueStore[-Key, +Value] + extends GenericReadKeyValueStore[UIO, Key, Value] + +trait WriteKeyValueStore[-Key, -Value] + extends GenericWriteKeyValueStore[UIO, Key, Value] + +trait KeyValueStore[-Key, Value] + extends GenericKeyValueStore[UIO, Key, Value] + with ReadKeyValueStore[Key, Value] + with WriteKeyValueStore[Key, Value] + +type StringKeyValueStore = KeyValueStore[String, String] + +object KeyValueStore: + extension (store: StringKeyValueStore) + /** Decode the value, ignoring decoding errors if any */ + def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = + store.get(key).map(_.flatMap(_.fromJson[A].toOption)) + + def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = + store.get(key).flatMap { + case None => ZIO.none + case Some(v) => + ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => + ZIO.log(err) *> ZIO.none + } + } + + def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = + store.put(key, value.toJson) diff --git a/service/shared/src/main/scala/works/iterative/core/service/Repository.scala b/service/shared/src/main/scala/works/iterative/core/service/Repository.scala new file mode 100644 index 0000000..c450279 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/Repository.scala @@ -0,0 +1,52 @@ +package works.iterative.core.service + +import zio.* +import zio.stream.* + +trait GenericLoadService[Eff[+_], -Key, +Value]: + type Op[A] = Eff[A] + def load(id: Key): Op[Option[Value]] + +trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: + def updates: Eff[Str[Key]] + +trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: + type Op[A] = Eff[A] + def loadAll(ids: Seq[Key]): Op[Coll[Value]] + +trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: + type Op[A] = Eff[A] + def find(filter: FilterArg): Op[Coll[Value]] + +trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] + extends GenericLoadService[Eff, Key, Value] + with GenericLoadAllService[Eff, Coll, Key, Value] + with GenericFindService[Eff, Coll, Key, Value, FilterArg] + +trait GenericWriteRepository[Eff[_], -Key, -Value]: + type Op[A] = Eff[A] + def save(key: Key, value: Value): Op[Unit] + +trait GenericRepository[Eff[+_], -Key, Value] + extends GenericReadRepository[Eff, List, Key, Value, Unit] + with GenericWriteRepository[Eff, Key, Value] + +type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] +type LoadAllRepository[-Key, +Value] = + GenericLoadAllService[UIO, List, Key, Value] + +trait ReadRepository[-Key, +Value, -FilterArg] + extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: + override def loadAll(ids: Seq[Key]): UIO[List[Value]] = + // Inefficient implementation, meant to be overridden + ZIO.foreach(ids)(load).map(_.flatten.toList) + +trait UpdateNotifyRepository[Key] + extends GenericUpdateNotifyService[UIO, UStream, Key] + +trait WriteRepository[-Key, -Value] + extends GenericWriteRepository[UIO, Key, Value] + +trait Repository[-Key, Value, -FilterArg] + extends ReadRepository[Key, Value, FilterArg] + with WriteRepository[Key, Value] diff --git a/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala new file mode 100644 index 0000000..66ad66b --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala @@ -0,0 +1,21 @@ +package works.iterative.core.service +package impl + +import works.iterative.core.FileSupport.* +import works.iterative.core.FileRef +import zio.* + +object InMemoryFileStore: + val layer: ULayer[FileStore] = ZLayer.succeed { + new FileStore: + override def store(file: FileRepr): Op[FileRef] = + ZIO.succeed(FileRef.unsafe(file.name, "#")) + override def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] = + ZIO.succeed(FileRef.unsafe(name, "#")) + override def load(url: String): Op[Option[Array[Byte]]] = + ZIO.succeed(None) + } diff --git a/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala new file mode 100644 index 0000000..e3b7d45 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala @@ -0,0 +1,17 @@ +package works.iterative.core.service +package impl + +import zio.* + +trait InMemoryRepository[Key, Value, FilterArg]( + data: Ref[Map[Key, Value]], + filter: FilterArg => Value => Boolean +) extends Repository[Key, Value, FilterArg]: + override def save(key: Key, value: Value): UIO[Unit] = + data.update(_ + (key -> value)) + override def load(key: Key): UIO[Option[Value]] = + data.get.map(_.get(key)) + override def loadAll(keys: Seq[Key]): UIO[List[Value]] = + data.get.map(_.view.filterKeys(keys.contains).values.toList) + override def find(filterArg: FilterArg): UIO[List[Value]] = + data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/service/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/service/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala new file mode 100644 index 0000000..358cb9d --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala @@ -0,0 +1,11 @@ +package works.iterative.core.service.impl + +import works.iterative.core.service.IdGenerator +import zio.* + +class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: + def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) + +object UUIDGenerator: + def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = + ZLayer.succeed(UUIDGenerator(f)) diff --git a/service/specs/shared/src/main/scala/works/iterative/core/service/specs/KeyValueStoreSpec.scala b/service/specs/shared/src/main/scala/works/iterative/core/service/specs/KeyValueStoreSpec.scala new file mode 100644 index 0000000..0548a3f --- /dev/null +++ b/service/specs/shared/src/main/scala/works/iterative/core/service/specs/KeyValueStoreSpec.scala @@ -0,0 +1,34 @@ +package works.iterative.core +package service +package specs + +import zio.* +import zio.test.* + +abstract class KeyValueStoreSpec extends ZIOSpecDefault: + val defaultSpec = suite("Consul spec")( + test("get returns None if key does not exist") { + for + kv <- ZIO.service[StringKeyValueStore] + uuid <- Random.nextUUID + key = s"test/$uuid" + value <- kv.get(key) + yield assertTrue(value.isEmpty) + }, + test("read/put/delete works") { + for + kv <- ZIO.service[StringKeyValueStore] + uuid <- Random.nextUUID + key = s"test/$uuid" + originalValue <- kv.get(key) + _ <- kv.put(key, "test_value") + updatedValue <- kv.get(key) + _ <- kv.remove(key) + removedValue <- kv.get(key) + yield assertTrue( + originalValue.isEmpty, + updatedValue.contains("test_value"), + removedValue.isEmpty + ) + } + ) diff --git a/tapir/shared/src/main/scala/works/iterative/tapir/ClientEndpointFactory.scala b/tapir/shared/src/main/scala/works/iterative/tapir/ClientEndpointFactory.scala index f232c35..d9d8b30 100644 --- a/tapir/shared/src/main/scala/works/iterative/tapir/ClientEndpointFactory.scala +++ b/tapir/shared/src/main/scala/works/iterative/tapir/ClientEndpointFactory.scala @@ -1,10 +1,12 @@ package works.iterative.tapir import zio.* -import sttp.tapir.PublicEndpoint +import sttp.tapir.Endpoint import sttp.capabilities.zio.ZioStreams import sttp.capabilities.WebSockets +import scala.compiletime.{erasedValue, summonFrom} + opaque type Client[I, E, O] = I => IO[E, O] object Client: @@ -14,26 +16,59 @@ def apply(i: I): IO[E, O] = f(i) def toEffect: I => ZIO[Any, E, O] = i => f(i) +type ClientError[E] = E match + case Unit => Nothing + case _ => E + +object ClientError: + inline def cause[E](e: Cause[E]): Cause[ClientError[E]] = + erasedValue[E] match + case _: Unit => + Cause.die(throw new IllegalStateException("Internal Server Error")) + case _ => e.asInstanceOf[Cause[ClientError[E]]] + + inline def apply[S, I, E, A]( + client: SecureClient[S, I, E, A] + ): SecureClient[S, I, ClientError[E], A] = + s => i => client(s)(i).mapErrorCause(cause(_)) + +opaque type SecureClient[S, I, E, O] = S => I => IO[E, O] + +object SecureClient: + def apply[S, I, E, O](f: S => I => IO[E, O]): SecureClient[S, I, E, O] = f + + extension [S, I, E, O](f: SecureClient[S, I, E, O]) + def apply(s: S): Client[I, E, O] = f(s) + def toEffect: S => I => ZIO[Any, E, O] = s => i => f(s)(i) + +type ClientResult[S, I, E, O] = S match + case Unit => I => IO[ClientError[E], O] + case _ => S => I => IO[ClientError[E], O] + +/** Create effectful methods to perform the endpoint operation + * + * Just a useful way to have something that will derive the client from the + * endpoint using other layers, like BaseUri and provided STTP Backend. + */ trait ClientEndpointFactory: - def umake[I, O]( - endpoint: PublicEndpoint[I, Unit, O, Any] - ): Client[I, Nothing, O] - def make[I, E, O]( - endpoint: PublicEndpoint[I, E, O, Any] - ): Client[I, E, O] - def stream[I, E, O]( - endpoint: PublicEndpoint[ - Unit, - E, - ZioStreams.Pipe[I, O], - ZioStreams & WebSockets - ] - ): Client[Unit, E, ZioStreams.Pipe[I, O]] - def ustream[I, O]( - endpoint: PublicEndpoint[ - Unit, - Unit, - ZioStreams.Pipe[I, O], - ZioStreams & WebSockets - ] - ): Client[Unit, Nothing, ZioStreams.Pipe[I, O]] + inline def makeSecure[S, I, E, O]( + endpoint: Endpoint[S, I, E, O, Any] + ): S => I => IO[ClientError[E], O] = + inline val isWebSocket = + erasedValue[O] match + case _: ZioStreams.Pipe[I, O] => true + case _ => false + + s => i => makeSecureClient(endpoint, isWebSocket)(s)(i).mapErrorCause(ClientError.cause(_)) + + transparent inline def make[S, I, E, O]( + endpoint: Endpoint[S, I, E, O, Any] + ): ClientResult[S, I, E, O] = + erasedValue[S] match + case _: Unit => makeSecure(endpoint)(().asInstanceOf[S]) + case _ => makeSecure(endpoint) + + def makeSecureClient[S, I, E, O]( + endpoint: Endpoint[S, I, E, O, ZioStreams & WebSockets], + isWebSocket: Boolean = false + ): S => I => IO[E, O] diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala new file mode 100644 index 0000000..65c7e92 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* + +/** Represents aggregate root in DDD + * + * @tparam Id + * Aggregate root id + * @tparam Command + * Aggregate root command + * @tparam Event + * Aggregate root event + * @tparam State + * Aggregate root state + */ +trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: + /** Aggregate root id */ + def id: Id + + /** Current state */ + def state: UIO[State] + + def handle(command: Command): IO[Error, Unit] diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala new file mode 100644 index 0000000..c051650 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala @@ -0,0 +1,58 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator + +sealed trait FactoryError: + def entityId: String + def userMessage: UserMessage + +object FactoryError: + case class EntityAlreadyExists[Id](entityId: String, id: Id) + extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) + + case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) + +trait AggregateRootFactory[ + Ident, + T <: AggregateRoot[Ident, ?, ?, ?, ?] +]: + type Id = Ident + type NotFound = FactoryError.EntityNotFound[Id] + type AlreadyExists = FactoryError.EntityAlreadyExists[Id] + + protected def AlreadyExists(id: Id): AlreadyExists = + FactoryError.EntityAlreadyExists(entityId, id) + + protected def NotFound(id: Id): NotFound = + FactoryError.EntityNotFound(entityId, id) + + def entityId: String + def make(id: Id): IO[AlreadyExists, T] + def load(id: Id): IO[NotFound, T] + + def loadOrMake(id: Id): UIO[T] = + load(id) + .orElse(make(id)) + .orDieWith(_ => + new RuntimeException( + s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" + ) + ) + + def make()(using idGen: IdGenerator[Id]): UIO[T] = + val tryToCreate = for + id <- idGen.nextId + entity <- make(id) + yield entity + + tryToCreate + .retryN(10) + .orDieWith(_ => + new RuntimeException( + "Cannot create entity, generator failed 10 times to create new ID" + ) + ) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala new file mode 100644 index 0000000..b2fef3a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala @@ -0,0 +1,24 @@ +package works.iterative.entity + +import works.iterative.core.Validated +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator +import zio.* + +/** Generic class for entity factories. + * + * Creates a new entity from a "seed". + */ +trait EntityFactory[Entity, Seed]: + def make(seed: Seed): IO[UserMessage, Entity] + +/** A factory using idGenerator to create the entity */ +class GeneratingFactory[Entity, Seed, Id]( + constructor: Seed => Validated[Id => Entity], + idGenerator: IdGenerator[Id] +) extends EntityFactory[Entity, Seed]: + override def make(seed: Seed): IO[UserMessage, Entity] = + for + construct <- constructor(seed).toZIO + id <- idGenerator.nextId + yield construct(id) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala new file mode 100644 index 0000000..29ae384 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.auth.CurrentUser + +trait EntityUpdateService[Id, Command, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def update(id: Id, command: Command): Op[Unit] + +trait EntityCreateService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(initData: Init): Op[Id] + +trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(id: Id, initData: Init): Op[Id] + +trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] + extends EntityCreateService[Id, Init, Error] + with EntityUpdateService[Id, Command, Error] diff --git a/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala new file mode 100644 index 0000000..946011a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import zio.* + +trait ViewProcessor[Event]: + def process(event: Event): UIO[Unit] diff --git a/entity/shared/src/main/scala/works/iterative/event/Event.scala b/entity/shared/src/main/scala/works/iterative/event/Event.scala new file mode 100644 index 0000000..7907ac0 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/Event.scala @@ -0,0 +1,4 @@ +package works.iterative.event + +trait Event[A]: + def record: EventRecord diff --git a/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala new file mode 100644 index 0000000..490417a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala @@ -0,0 +1,15 @@ +package works.iterative.event + +import works.iterative.core.* +import zio.* + +/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ +final case class EventRecord(userHandle: UserHandle, timestamp: Moment): + val displayName: String = userHandle.displayName + +object EventRecord: + def apply(userHandle: UserHandle): UIO[EventRecord] = + Moment.now.map(EventRecord(userHandle, _)) + + def now(using userHandle: UserHandle): UIO[EventRecord] = + apply(userHandle) diff --git a/entity/shared/src/main/scala/works/iterative/event/EventStore.scala b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala new file mode 100644 index 0000000..3226df3 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala @@ -0,0 +1,13 @@ +package works.iterative.event + +import zio.* + +/** Abstraction of event log + * + * The log can persist events and restore the state of an entity from the + * events. + */ +trait EventStore[Id, T <: Event[_]]: + type Op[A] = UIO[A] + def persist(id: Id, event: T): Op[Unit] + def get(id: Id): Op[Seq[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala new file mode 100644 index 0000000..6ccccd7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala @@ -0,0 +1,12 @@ +package works.iterative.event + +import zio.* + +/** Snapshot store + * + * Stores a snapshot of an entity. + */ +trait SnapshotStore[Id, T]: + type Op[A] = UIO[A] + def update(id: Id, snapshot: T): Op[Unit] + def get(id: Id): Op[Option[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala new file mode 100644 index 0000000..964d420 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala @@ -0,0 +1,21 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) + extends EventStore[Id, T]: + override def persist(id: Id, event: T): Op[Unit] = + data.update { m => + val events = m.getOrElse(id, Nil) + m.updated(id, event :: events) + }.unit + override def get(id: Id): Op[Seq[T]] = + data.get.map(_.getOrElse(id, Nil)) + +object InMemoryEventStore: + def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, List[T]]) + yield InMemoryEventStore(data) + } diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala new file mode 100644 index 0000000..722d7d7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala @@ -0,0 +1,19 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) + extends SnapshotStore[Id, T]: + override def update(id: Id, snapshot: T): UIO[Unit] = + data.update(_.updated(id, snapshot)).unit + + override def get(id: Id): UIO[Option[T]] = + data.get.map(_.get(id)) + +object InMemorySnapshotStore: + def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, T]) + yield InMemorySnapshotStore(data) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala new file mode 100644 index 0000000..874d16c --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala @@ -0,0 +1,63 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import sttp.client3.* + +class ConsulKeyValueStore( + token: ConsulToken, + baseUri: BaseUri, + backend: CustomTapir.Backend +) extends KeyValueStore[String, String]: + + private val defaultBase = "http://localhost:8500" + private val base = baseUri.value.map(_.toString).getOrElse(defaultBase) + + private def addAuth[U[_], T, R]( + request: RequestT[U, T, R] + ): RequestT[U, T, R] = + request.header("X-Consul-Token", token.token) + + override def get(key: String): UIO[Option[String]] = { + for + response <- addAuth(basicRequest.get(uri"$base/v1/kv/${key}?raw=true")) + .send(backend) + yield response.body.toOption + }.orDie + + override def put(key: String, value: String): UIO[Unit] = { + addAuth(basicRequest.put(uri"$base/v1/kv/${key}").body(value)) + .send(backend) + .unit + }.orDie + + override def remove(key: String): UIO[Unit] = { + addAuth(basicRequest.delete(uri"$base/v1/kv/${key}")) + .send(backend) + .unit + }.orDie + +object ConsulKeyValueStore: + val layer: URLayer[ + ConsulToken & BaseUri & CustomTapir.Backend, + StringKeyValueStore + ] = + ZLayer { + for + token <- ZIO.service[ConsulToken] + baseUri <- ZIO.service[BaseUri] + backend <- ZIO.service[CustomTapir.Backend] + yield ConsulKeyValueStore(token, baseUri, backend) + } + + val fromEnv + : ZLayer[CustomTapir.Backend, SecurityException, StringKeyValueStore] = + ConsulToken.fromEnv >>> ZLayer { + for + token <- ZIO.service[ConsulToken] + backend <- ZIO.service[CustomTapir.Backend] + addr <- System.envOrElse("CONSUL_ADDR", "http://localhost:8500") + yield ConsulKeyValueStore(token, BaseUri(addr), backend) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala new file mode 100644 index 0000000..5c5483a --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala @@ -0,0 +1,150 @@ +package works.iterative.core.service.impl + +import zio.* +import sttp.tapir.EndpointIO.annotations.* +import sttp.tapir.Schema +import zio.json.* + +final case class ConsulToken(token: Option[String]) + +object ConsulToken: + def fromEnv: ZLayer[Any, SecurityException, ConsulToken] = ZLayer { + System.env("CONSUL_TOKEN").map(ConsulToken(_)) + } + +@jsonMemberNames(PascalCase) +final case class ConsulMetadata( + createIndex: Long, + modifyIndex: Long, + lockIndex: Long, + key: String, + flags: Long, + value: String, + session: Option[String] +) derives JsonCodec, + Schema + +@endpointInput("v1/kv/{key}") +final case class GetQuery( + @path + @description("Key to get from the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + // TODO: This would not work type-wise properly, as the result type of the call would differ depending on the query parameters + // These are missing, split to different queries to enable. + /* + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies the response is just the raw value of the key, without any encoding or metadata." + ) + raw: Option[Boolean] = None, + @query + @description( + "Specifies to return only keys (no values or metadata). Specifying this parameter implies recurse." + ) + keys: Option[Boolean] = None, + @query + @description( + "Specifies the string to use as a separator for recursive key lookups. This option is only used when paired with the keys parameter to limit the prefix of keys returned, only up to the given separator." + ) + separator: Option[String] = None, + */ + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class PutQuery( + @path + @description("Key to put into the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies an unsigned value between 0 and (2^64)-1 to store with the key. API consumers can use this field any way they choose for their application." + ) + flags: Option[Long] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Supply a session ID to use in a lock acquisition operation.") + acquire: Option[String] = None, + @query + @description("Supply a session ID to use in a release operation.") + release: Option[String] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class DeleteQuery( + @path + @description("Key to delete in the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +/* +sealed trait ConsulGetError +object ConsulGetError: + case object NotFound extends ConsulGetError derives JsonCodec, Schema + case object Forbidden extends ConsulGetError derives JsonCodec, Schema + case class ServerError(msg: String) extends ConsulGetError derives JsonCodec, Schema + +trait ConsulKVEndpoints extends CustomTapir: + private val kvBase = endpoint + .in("v1" / "kv") + .securityIn( + auth.apiKey(header[Option[String]]("X-Consul-Token").mapTo[ConsulToken]) + ) + + val getKV: Endpoint[ConsulToken, GetQuery, Unit, ConsulMetadata, Any] = + kvBase.get + .in(EndpointInput.derived[GetQuery]) + .out(jsonBody[ConsulMetadata]) + .errorOut( + oneOf[ConsulGetError]( + oneOfVariant( + statusCode(StatusCode.NotFound).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfVariant( + statusCode(StatusCode.Forbidden).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfDefaultVariant( + statusCode(StatusCode.InternalServerError).and(jsonBody[ServerError].description("not found")) + ) + ) + ) + val putKV: Endpoint[ConsulToken, PutQuery, Unit, Boolean, Any] = + kvBase.put.in(EndpointInput.derived[PutQuery]).out(jsonBody[Boolean]) + val deleteKV: Endpoint[ConsulToken, DeleteQuery, Unit, Boolean, Any] = + kvBase.delete.in(EndpointInput.derived[DeleteQuery]).out(jsonBody[Boolean]) + +*/ \ No newline at end of file diff --git a/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala new file mode 100644 index 0000000..c938083 --- /dev/null +++ b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala @@ -0,0 +1,19 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import zio.test.* + +object ConsulKeyValueStoreSpec extends specs.KeyValueStoreSpec: + override def spec = defaultSpec.provide( + ConsulKeyValueStore.layer, + CustomTapir.clientLayer, + ZLayer { + Live.live(System.env("CONSUL_ADDR")).map(a => BaseUri(a.get)) + }, + ZLayer { + Live.live(System.env("CONSUL_TOKEN")).map(ConsulToken(_)) + } + ) @@ TestAspect.ifEnvSet("CONSUL_ADDR") diff --git a/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala new file mode 100644 index 0000000..16211a8 --- /dev/null +++ b/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala @@ -0,0 +1,29 @@ +package works.iterative.core.service.impl + +import org.scalajs.dom.Storage +import works.iterative.core.service.Repository +import zio.* +import zio.json.* + +// TODO: improve error reporting on generic repositories +// This is good just for prototypes +// And it cannot be used to query data, as there is no way to iterate the storage +class JsStorageRepository[Value: JsonCodec]( + storage: Storage +) extends Repository[String, Value, String]: + + override def load(id: String): UIO[Option[Value]] = { + for + raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) + data <- ZIO.foreach(raw) { r => + ZIO + .fromEither(r.fromJson[Value]) + .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) + } + yield data + }.orDie + + override def save(key: String, value: Value): UIO[Unit] = + ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie + + override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala new file mode 100644 index 0000000..8c88818 --- /dev/null +++ b/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala @@ -0,0 +1,26 @@ +package works.iterative.core +package service +package impl + +import zio.* +import zio.stream.* + +class JcaDigestService extends DigestService: + + override def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): IO[E, Digest] = + for + md <- ZIO + .attempt( + java.security.MessageDigest.getInstance(algorithm.value) + ) + .orDie + _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) + digest <- ZIO.attempt(md.digest()).orDie + yield Digest(algorithm, digest) + +object JcaDigestGenerator: + val layer: ULayer[DigestService] = + ZLayer.succeed(new JcaDigestService) diff --git a/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala new file mode 100644 index 0000000..883bd7a --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala @@ -0,0 +1,26 @@ +package works.iterative.core.service + +import works.iterative.core.Digest +import works.iterative.core.DigestAlgorithm + +import zio.* +import zio.stream.* + +trait DigestService: + def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): IO[E, Digest] + +object DigestService: + def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): ZIO[DigestService, E, Digest] = + ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) + + def digest( + algorithm: DigestAlgorithm, + content: Array[Byte] + ): URIO[DigestService, Digest] = + digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala new file mode 100644 index 0000000..10bdeb7 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala @@ -0,0 +1,29 @@ +package works.iterative.core.service + +import works.iterative.core.FileSupport.* +import works.iterative.core.FileRef + +import zio.* + +trait FileStore: + type Op[A] = UIO[A] + def store(file: FileRepr): Op[FileRef] + def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] + def load(url: String): Op[Option[Array[Byte]]] + +object FileStore: + type Op[A] = URIO[FileStore, A] + def store(file: FileRepr): Op[FileRef] = + ZIO.serviceWithZIO(_.store(file)) + def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] = + ZIO.serviceWithZIO(_.store(name, file, contentType)) + def load(url: String): Op[Option[Array[Byte]]] = + ZIO.serviceWithZIO(_.load(url)) diff --git a/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala new file mode 100644 index 0000000..1779da3 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala @@ -0,0 +1,10 @@ +package works.iterative.core.service + +import zio.* + +/** Generator of unique IDs of a given type */ +trait IdGenerator[A]: + self => + def nextId: UIO[A] + def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: + def nextId: UIO[B] = self.nextId.map(f) diff --git a/service/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/service/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala new file mode 100644 index 0000000..23d1908 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala @@ -0,0 +1,47 @@ +package works.iterative.core.service + +import zio.* +import zio.json.* + +trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: + type Op[A] = Eff[A] + def get(key: Key): Eff[Option[Value]] + +trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: + def put(key: Key, value: Value): Eff[Unit] + def remove(key: Key): Eff[Unit] + +trait GenericKeyValueStore[Eff[+_], -Key, Value] + extends GenericReadKeyValueStore[Eff, Key, Value] + with GenericWriteKeyValueStore[Eff, Key, Value] + +trait ReadKeyValueStore[-Key, +Value] + extends GenericReadKeyValueStore[UIO, Key, Value] + +trait WriteKeyValueStore[-Key, -Value] + extends GenericWriteKeyValueStore[UIO, Key, Value] + +trait KeyValueStore[-Key, Value] + extends GenericKeyValueStore[UIO, Key, Value] + with ReadKeyValueStore[Key, Value] + with WriteKeyValueStore[Key, Value] + +type StringKeyValueStore = KeyValueStore[String, String] + +object KeyValueStore: + extension (store: StringKeyValueStore) + /** Decode the value, ignoring decoding errors if any */ + def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = + store.get(key).map(_.flatMap(_.fromJson[A].toOption)) + + def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = + store.get(key).flatMap { + case None => ZIO.none + case Some(v) => + ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => + ZIO.log(err) *> ZIO.none + } + } + + def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = + store.put(key, value.toJson) diff --git a/service/shared/src/main/scala/works/iterative/core/service/Repository.scala b/service/shared/src/main/scala/works/iterative/core/service/Repository.scala new file mode 100644 index 0000000..c450279 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/Repository.scala @@ -0,0 +1,52 @@ +package works.iterative.core.service + +import zio.* +import zio.stream.* + +trait GenericLoadService[Eff[+_], -Key, +Value]: + type Op[A] = Eff[A] + def load(id: Key): Op[Option[Value]] + +trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: + def updates: Eff[Str[Key]] + +trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: + type Op[A] = Eff[A] + def loadAll(ids: Seq[Key]): Op[Coll[Value]] + +trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: + type Op[A] = Eff[A] + def find(filter: FilterArg): Op[Coll[Value]] + +trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] + extends GenericLoadService[Eff, Key, Value] + with GenericLoadAllService[Eff, Coll, Key, Value] + with GenericFindService[Eff, Coll, Key, Value, FilterArg] + +trait GenericWriteRepository[Eff[_], -Key, -Value]: + type Op[A] = Eff[A] + def save(key: Key, value: Value): Op[Unit] + +trait GenericRepository[Eff[+_], -Key, Value] + extends GenericReadRepository[Eff, List, Key, Value, Unit] + with GenericWriteRepository[Eff, Key, Value] + +type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] +type LoadAllRepository[-Key, +Value] = + GenericLoadAllService[UIO, List, Key, Value] + +trait ReadRepository[-Key, +Value, -FilterArg] + extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: + override def loadAll(ids: Seq[Key]): UIO[List[Value]] = + // Inefficient implementation, meant to be overridden + ZIO.foreach(ids)(load).map(_.flatten.toList) + +trait UpdateNotifyRepository[Key] + extends GenericUpdateNotifyService[UIO, UStream, Key] + +trait WriteRepository[-Key, -Value] + extends GenericWriteRepository[UIO, Key, Value] + +trait Repository[-Key, Value, -FilterArg] + extends ReadRepository[Key, Value, FilterArg] + with WriteRepository[Key, Value] diff --git a/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala new file mode 100644 index 0000000..66ad66b --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala @@ -0,0 +1,21 @@ +package works.iterative.core.service +package impl + +import works.iterative.core.FileSupport.* +import works.iterative.core.FileRef +import zio.* + +object InMemoryFileStore: + val layer: ULayer[FileStore] = ZLayer.succeed { + new FileStore: + override def store(file: FileRepr): Op[FileRef] = + ZIO.succeed(FileRef.unsafe(file.name, "#")) + override def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] = + ZIO.succeed(FileRef.unsafe(name, "#")) + override def load(url: String): Op[Option[Array[Byte]]] = + ZIO.succeed(None) + } diff --git a/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala new file mode 100644 index 0000000..e3b7d45 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala @@ -0,0 +1,17 @@ +package works.iterative.core.service +package impl + +import zio.* + +trait InMemoryRepository[Key, Value, FilterArg]( + data: Ref[Map[Key, Value]], + filter: FilterArg => Value => Boolean +) extends Repository[Key, Value, FilterArg]: + override def save(key: Key, value: Value): UIO[Unit] = + data.update(_ + (key -> value)) + override def load(key: Key): UIO[Option[Value]] = + data.get.map(_.get(key)) + override def loadAll(keys: Seq[Key]): UIO[List[Value]] = + data.get.map(_.view.filterKeys(keys.contains).values.toList) + override def find(filterArg: FilterArg): UIO[List[Value]] = + data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/service/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/service/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala new file mode 100644 index 0000000..358cb9d --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala @@ -0,0 +1,11 @@ +package works.iterative.core.service.impl + +import works.iterative.core.service.IdGenerator +import zio.* + +class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: + def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) + +object UUIDGenerator: + def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = + ZLayer.succeed(UUIDGenerator(f)) diff --git a/service/specs/shared/src/main/scala/works/iterative/core/service/specs/KeyValueStoreSpec.scala b/service/specs/shared/src/main/scala/works/iterative/core/service/specs/KeyValueStoreSpec.scala new file mode 100644 index 0000000..0548a3f --- /dev/null +++ b/service/specs/shared/src/main/scala/works/iterative/core/service/specs/KeyValueStoreSpec.scala @@ -0,0 +1,34 @@ +package works.iterative.core +package service +package specs + +import zio.* +import zio.test.* + +abstract class KeyValueStoreSpec extends ZIOSpecDefault: + val defaultSpec = suite("Consul spec")( + test("get returns None if key does not exist") { + for + kv <- ZIO.service[StringKeyValueStore] + uuid <- Random.nextUUID + key = s"test/$uuid" + value <- kv.get(key) + yield assertTrue(value.isEmpty) + }, + test("read/put/delete works") { + for + kv <- ZIO.service[StringKeyValueStore] + uuid <- Random.nextUUID + key = s"test/$uuid" + originalValue <- kv.get(key) + _ <- kv.put(key, "test_value") + updatedValue <- kv.get(key) + _ <- kv.remove(key) + removedValue <- kv.get(key) + yield assertTrue( + originalValue.isEmpty, + updatedValue.contains("test_value"), + removedValue.isEmpty + ) + } + ) diff --git a/tapir/shared/src/main/scala/works/iterative/tapir/ClientEndpointFactory.scala b/tapir/shared/src/main/scala/works/iterative/tapir/ClientEndpointFactory.scala index f232c35..d9d8b30 100644 --- a/tapir/shared/src/main/scala/works/iterative/tapir/ClientEndpointFactory.scala +++ b/tapir/shared/src/main/scala/works/iterative/tapir/ClientEndpointFactory.scala @@ -1,10 +1,12 @@ package works.iterative.tapir import zio.* -import sttp.tapir.PublicEndpoint +import sttp.tapir.Endpoint import sttp.capabilities.zio.ZioStreams import sttp.capabilities.WebSockets +import scala.compiletime.{erasedValue, summonFrom} + opaque type Client[I, E, O] = I => IO[E, O] object Client: @@ -14,26 +16,59 @@ def apply(i: I): IO[E, O] = f(i) def toEffect: I => ZIO[Any, E, O] = i => f(i) +type ClientError[E] = E match + case Unit => Nothing + case _ => E + +object ClientError: + inline def cause[E](e: Cause[E]): Cause[ClientError[E]] = + erasedValue[E] match + case _: Unit => + Cause.die(throw new IllegalStateException("Internal Server Error")) + case _ => e.asInstanceOf[Cause[ClientError[E]]] + + inline def apply[S, I, E, A]( + client: SecureClient[S, I, E, A] + ): SecureClient[S, I, ClientError[E], A] = + s => i => client(s)(i).mapErrorCause(cause(_)) + +opaque type SecureClient[S, I, E, O] = S => I => IO[E, O] + +object SecureClient: + def apply[S, I, E, O](f: S => I => IO[E, O]): SecureClient[S, I, E, O] = f + + extension [S, I, E, O](f: SecureClient[S, I, E, O]) + def apply(s: S): Client[I, E, O] = f(s) + def toEffect: S => I => ZIO[Any, E, O] = s => i => f(s)(i) + +type ClientResult[S, I, E, O] = S match + case Unit => I => IO[ClientError[E], O] + case _ => S => I => IO[ClientError[E], O] + +/** Create effectful methods to perform the endpoint operation + * + * Just a useful way to have something that will derive the client from the + * endpoint using other layers, like BaseUri and provided STTP Backend. + */ trait ClientEndpointFactory: - def umake[I, O]( - endpoint: PublicEndpoint[I, Unit, O, Any] - ): Client[I, Nothing, O] - def make[I, E, O]( - endpoint: PublicEndpoint[I, E, O, Any] - ): Client[I, E, O] - def stream[I, E, O]( - endpoint: PublicEndpoint[ - Unit, - E, - ZioStreams.Pipe[I, O], - ZioStreams & WebSockets - ] - ): Client[Unit, E, ZioStreams.Pipe[I, O]] - def ustream[I, O]( - endpoint: PublicEndpoint[ - Unit, - Unit, - ZioStreams.Pipe[I, O], - ZioStreams & WebSockets - ] - ): Client[Unit, Nothing, ZioStreams.Pipe[I, O]] + inline def makeSecure[S, I, E, O]( + endpoint: Endpoint[S, I, E, O, Any] + ): S => I => IO[ClientError[E], O] = + inline val isWebSocket = + erasedValue[O] match + case _: ZioStreams.Pipe[I, O] => true + case _ => false + + s => i => makeSecureClient(endpoint, isWebSocket)(s)(i).mapErrorCause(ClientError.cause(_)) + + transparent inline def make[S, I, E, O]( + endpoint: Endpoint[S, I, E, O, Any] + ): ClientResult[S, I, E, O] = + erasedValue[S] match + case _: Unit => makeSecure(endpoint)(().asInstanceOf[S]) + case _ => makeSecure(endpoint) + + def makeSecureClient[S, I, E, O]( + endpoint: Endpoint[S, I, E, O, ZioStreams & WebSockets], + isWebSocket: Boolean = false + ): S => I => IO[E, O] diff --git a/tapir/shared/src/main/scala/works/iterative/tapir/LiveClientEndpointFactory.scala b/tapir/shared/src/main/scala/works/iterative/tapir/LiveClientEndpointFactory.scala index 7657048..58de66c 100644 --- a/tapir/shared/src/main/scala/works/iterative/tapir/LiveClientEndpointFactory.scala +++ b/tapir/shared/src/main/scala/works/iterative/tapir/LiveClientEndpointFactory.scala @@ -1,7 +1,7 @@ package works.iterative.tapir import zio.* -import sttp.tapir.PublicEndpoint +import sttp.tapir.Endpoint import sttp.capabilities.zio.ZioStreams import sttp.capabilities.WebSockets import sttp.tapir.client.sttp.ws.zio.* @@ -12,49 +12,11 @@ ) extends ClientEndpointFactory with CustomTapir: - override def umake[I, O]( - endpoint: PublicEndpoint[I, Unit, O, Any] - ): Client[I, Nothing, O] = Client((input: I) => - mkClient(endpoint)(input).orDieWith(_ => - new IllegalStateException("Internal Server Error") - ) - ) - - override def make[I, E, O]( - endpoint: PublicEndpoint[I, E, O, Any] - ): Client[I, E, O] = mkClient(endpoint) - - override def stream[I, E, O]( - endpoint: PublicEndpoint[ - Unit, - E, - ZioStreams.Pipe[I, O], - ZioStreams & WebSockets - ] - ): Client[Unit, E, ZioStreams.Pipe[I, O]] = - mkClient(endpoint, true) - - override def ustream[I, O]( - endpoint: PublicEndpoint[ - Unit, - Unit, - ZioStreams.Pipe[I, O], - ZioStreams & WebSockets - ] - ): Client[Unit, Nothing, ZioStreams.Pipe[I, O]] = Client( - mkClient(endpoint, true)(_).orDieWith(_ => - new IllegalStateException("Internal Server Error") - ) - ) - - private def mkClient[I, E, O]( - endpoint: PublicEndpoint[I, E, O, ZioStreams & WebSockets], + override def makeSecureClient[S, I, E, O]( + endpoint: Endpoint[S, I, E, O, ZioStreams & WebSockets], isWebSocket: Boolean = false - )(using - baseUri: BaseUri, - backend: Backend - ): Client[I, E, O] = Client((input: I) => - val req = toRequest( + ): S => I => IO[E, O] = (securityInput: S) => (input: I) => + val req = toSecureRequest( endpoint, if isWebSocket then baseUri.toUri.map(b => @@ -64,8 +26,10 @@ ) else baseUri.toUri ) - val fetch = req(input).followRedirects(false).send(backend) - for + + val fetch = req(securityInput)(input).followRedirects(false).send(backend) + + val result = for resp <- fetch.orDie body <- resp.body match case DecodeResult.Value(v) => ZIO.succeed(v) @@ -77,7 +41,8 @@ ) v <- ZIO.fromEither(body) yield v - ) + + result object LiveClientEndpointFactory: val layer: URLayer[BaseUri & CustomTapir.Backend, ClientEndpointFactory] = diff --git a/.sbtopts b/.sbtopts index 6913385..49e7a06 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --mem 2048 -J-XX:+UseG1GC +-mem 4096 -J-XX:+UseG1GC diff --git a/build.sbt b/build.sbt index d67beb8..7cc629d 100644 --- a/build.sbt +++ b/build.sbt @@ -23,10 +23,32 @@ ) ) +lazy val service = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service") + .in(file("service")) + .settings(IWDeps.useZIO()) + .dependsOn(core) + +lazy val entity = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-entity") + .in(file("entity")) + .settings(IWDeps.useZIO()) + .dependsOn(core, service) + +lazy val `service-specs` = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .settings(name := "iw-support-service-specs") + .in(file("service/specs")) + .settings(IWDeps.useZIO(), IWDeps.useZIOTest(Compile)) + .dependsOn(service) + lazy val `tapir-support` = crossProject(JSPlatform, JVMPlatform) .in(file("tapir")) .settings(name := "iw-support-tapir") .settings( + IWDeps.useZIO(), IWDeps.tapirCore, IWDeps.tapirZIOJson, IWDeps.useZIOJson, @@ -46,6 +68,15 @@ ) .dependsOn(core) +lazy val hashicorp = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("hashicorp")) + .settings(name := "iw-support-hashicorp") + .settings( + IWDeps.useZIO() + ) + .dependsOn(service, `service-specs`, `tapir-support`) + lazy val codecs = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("codecs")) @@ -55,7 +86,7 @@ excludeDependencies += // Gets transitively dragged in by zio-nio, conflicting with _3 ExclusionRule("org.scala-lang.modules", "scala-collection-compat_2.13") ) - .dependsOn(core, `tapir-support`) + .dependsOn(core, entity, `tapir-support`) lazy val `mongo-support` = project .in(file("mongo")) @@ -88,7 +119,7 @@ IWDeps.akka.profiles.eventsourcedJdbcProjection, libraryDependencies += "com.github.ghik" %% "silencer-lib" % "1.4.2" % Provided cross CrossVersion.for3Use2_13 ) - .dependsOn(core.jvm) + .dependsOn(core.jvm, entity.jvm) lazy val ui = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Full) @@ -170,6 +201,12 @@ .aggregate( core.js, core.jvm, + entity.js, + entity.jvm, + service.js, + service.jvm, + `service-specs`.jvm, + hashicorp.jvm, codecs.js, codecs.jvm, `tapir-support`.js, diff --git a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala deleted file mode 100644 index 16211a8..0000000 --- a/core/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service.impl - -import org.scalajs.dom.Storage -import works.iterative.core.service.Repository -import zio.* -import zio.json.* - -// TODO: improve error reporting on generic repositories -// This is good just for prototypes -// And it cannot be used to query data, as there is no way to iterate the storage -class JsStorageRepository[Value: JsonCodec]( - storage: Storage -) extends Repository[String, Value, String]: - - override def load(id: String): UIO[Option[Value]] = { - for - raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) - data <- ZIO.foreach(raw) { r => - ZIO - .fromEither(r.fromJson[Value]) - .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) - } - yield data - }.orDie - - override def save(key: String, value: Value): UIO[Unit] = - ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie - - override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala deleted file mode 100644 index 8c88818..0000000 --- a/core/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core -package service -package impl - -import zio.* -import zio.stream.* - -class JcaDigestService extends DigestService: - - override def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] = - for - md <- ZIO - .attempt( - java.security.MessageDigest.getInstance(algorithm.value) - ) - .orDie - _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) - digest <- ZIO.attempt(md.digest()).orDie - yield Digest(algorithm, digest) - -object JcaDigestGenerator: - val layer: ULayer[DigestService] = - ZLayer.succeed(new JcaDigestService) diff --git a/core/shared/src/main/scala/works/iterative/core/Digest.scala b/core/shared/src/main/scala/works/iterative/core/Digest.scala index e61a4fc..ff9d1f1 100644 --- a/core/shared/src/main/scala/works/iterative/core/Digest.scala +++ b/core/shared/src/main/scala/works/iterative/core/Digest.scala @@ -1,9 +1,5 @@ package works.iterative.core -import works.iterative.core.service.DigestService - -import zio.* - opaque type DigestAlgorithm = String object DigestAlgorithm: @@ -15,9 +11,3 @@ value: Array[Byte] ) -object Digest: - def compute( - algorithm: DigestAlgorithm, - value: Array[Byte] - ): URIO[DigestService, Digest] = - DigestService.digest(algorithm, value) diff --git a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala deleted file mode 100644 index 883bd7a..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/DigestService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.Digest -import works.iterative.core.DigestAlgorithm - -import zio.* -import zio.stream.* - -trait DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): IO[E, Digest] - -object DigestService: - def digest[E]( - algorithm: DigestAlgorithm, - content: Stream[E, Byte] - ): ZIO[DigestService, E, Digest] = - ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) - - def digest( - algorithm: DigestAlgorithm, - content: Array[Byte] - ): URIO[DigestService, Digest] = - digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala deleted file mode 100644 index 10bdeb7..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/FileStore.scala +++ /dev/null @@ -1,29 +0,0 @@ -package works.iterative.core.service - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef - -import zio.* - -trait FileStore: - type Op[A] = UIO[A] - def store(file: FileRepr): Op[FileRef] - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] - def load(url: String): Op[Option[Array[Byte]]] - -object FileStore: - type Op[A] = URIO[FileStore, A] - def store(file: FileRepr): Op[FileRef] = - ZIO.serviceWithZIO(_.store(file)) - def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.serviceWithZIO(_.store(name, file, contentType)) - def load(url: String): Op[Option[Array[Byte]]] = - ZIO.serviceWithZIO(_.load(url)) diff --git a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala deleted file mode 100644 index 1779da3..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala +++ /dev/null @@ -1,10 +0,0 @@ -package works.iterative.core.service - -import zio.* - -/** Generator of unique IDs of a given type */ -trait IdGenerator[A]: - self => - def nextId: UIO[A] - def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: - def nextId: UIO[B] = self.nextId.map(f) diff --git a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala deleted file mode 100644 index d75845d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala +++ /dev/null @@ -1,46 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.json.* - -trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def get(key: Key): Eff[Option[Value]] - -trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: - def put(key: Key, value: Value): Eff[Unit] - -trait GenericKeyValueStore[Eff[+_], -Key, Value] - extends GenericReadKeyValueStore[Eff, Key, Value] - with GenericWriteKeyValueStore[Eff, Key, Value] - -trait ReadKeyValueStore[-Key, +Value] - extends GenericReadKeyValueStore[UIO, Key, Value] - -trait WriteKeyValueStore[-Key, -Value] - extends GenericWriteKeyValueStore[UIO, Key, Value] - -trait KeyValueStore[-Key, Value] - extends GenericKeyValueStore[UIO, Key, Value] - with ReadKeyValueStore[Key, Value] - with WriteKeyValueStore[Key, Value] - -type StringKeyValueStore = KeyValueStore[String, String] - -object KeyValueStore: - extension (store: StringKeyValueStore) - /** Decode the value, ignoring decoding errors if any */ - def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).map(_.flatMap(_.fromJson[A].toOption)) - - def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = - store.get(key).flatMap { - case None => ZIO.none - case Some(v) => - ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => - ZIO.log(err) *> ZIO.none - } - } - - def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = - store.put(key, value.toJson) diff --git a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala b/core/shared/src/main/scala/works/iterative/core/service/Repository.scala deleted file mode 100644 index c450279..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/Repository.scala +++ /dev/null @@ -1,52 +0,0 @@ -package works.iterative.core.service - -import zio.* -import zio.stream.* - -trait GenericLoadService[Eff[+_], -Key, +Value]: - type Op[A] = Eff[A] - def load(id: Key): Op[Option[Value]] - -trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: - def updates: Eff[Str[Key]] - -trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: - type Op[A] = Eff[A] - def loadAll(ids: Seq[Key]): Op[Coll[Value]] - -trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: - type Op[A] = Eff[A] - def find(filter: FilterArg): Op[Coll[Value]] - -trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] - extends GenericLoadService[Eff, Key, Value] - with GenericLoadAllService[Eff, Coll, Key, Value] - with GenericFindService[Eff, Coll, Key, Value, FilterArg] - -trait GenericWriteRepository[Eff[_], -Key, -Value]: - type Op[A] = Eff[A] - def save(key: Key, value: Value): Op[Unit] - -trait GenericRepository[Eff[+_], -Key, Value] - extends GenericReadRepository[Eff, List, Key, Value, Unit] - with GenericWriteRepository[Eff, Key, Value] - -type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] -type LoadAllRepository[-Key, +Value] = - GenericLoadAllService[UIO, List, Key, Value] - -trait ReadRepository[-Key, +Value, -FilterArg] - extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: - override def loadAll(ids: Seq[Key]): UIO[List[Value]] = - // Inefficient implementation, meant to be overridden - ZIO.foreach(ids)(load).map(_.flatten.toList) - -trait UpdateNotifyRepository[Key] - extends GenericUpdateNotifyService[UIO, UStream, Key] - -trait WriteRepository[-Key, -Value] - extends GenericWriteRepository[UIO, Key, Value] - -trait Repository[-Key, Value, -FilterArg] - extends ReadRepository[Key, Value, FilterArg] - with WriteRepository[Key, Value] diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala deleted file mode 100644 index 66ad66b..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.core.service -package impl - -import works.iterative.core.FileSupport.* -import works.iterative.core.FileRef -import zio.* - -object InMemoryFileStore: - val layer: ULayer[FileStore] = ZLayer.succeed { - new FileStore: - override def store(file: FileRepr): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(file.name, "#")) - override def store( - name: String, - file: Array[Byte], - contentType: Option[String] - ): Op[FileRef] = - ZIO.succeed(FileRef.unsafe(name, "#")) - override def load(url: String): Op[Option[Array[Byte]]] = - ZIO.succeed(None) - } diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala deleted file mode 100644 index e3b7d45..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala +++ /dev/null @@ -1,17 +0,0 @@ -package works.iterative.core.service -package impl - -import zio.* - -trait InMemoryRepository[Key, Value, FilterArg]( - data: Ref[Map[Key, Value]], - filter: FilterArg => Value => Boolean -) extends Repository[Key, Value, FilterArg]: - override def save(key: Key, value: Value): UIO[Unit] = - data.update(_ + (key -> value)) - override def load(key: Key): UIO[Option[Value]] = - data.get.map(_.get(key)) - override def loadAll(keys: Seq[Key]): UIO[List[Value]] = - data.get.map(_.view.filterKeys(keys.contains).values.toList) - override def find(filterArg: FilterArg): UIO[List[Value]] = - data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala deleted file mode 100644 index 358cb9d..0000000 --- a/core/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala +++ /dev/null @@ -1,11 +0,0 @@ -package works.iterative.core.service.impl - -import works.iterative.core.service.IdGenerator -import zio.* - -class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: - def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) - -object UUIDGenerator: - def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = - ZLayer.succeed(UUIDGenerator(f)) diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala deleted file mode 100644 index 9b8f18c..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateError.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.UserMessage - -trait AggregateError: - def userMessage: UserMessage diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala deleted file mode 100644 index 65c7e92..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* - -/** Represents aggregate root in DDD - * - * @tparam Id - * Aggregate root id - * @tparam Command - * Aggregate root command - * @tparam Event - * Aggregate root event - * @tparam State - * Aggregate root state - */ -trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: - /** Aggregate root id */ - def id: Id - - /** Current state */ - def state: UIO[State] - - def handle(command: Command): IO[Error, Unit] diff --git a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala deleted file mode 100644 index c051650..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala +++ /dev/null @@ -1,58 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator - -sealed trait FactoryError: - def entityId: String - def userMessage: UserMessage - -object FactoryError: - case class EntityAlreadyExists[Id](entityId: String, id: Id) - extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) - - case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: - def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) - -trait AggregateRootFactory[ - Ident, - T <: AggregateRoot[Ident, ?, ?, ?, ?] -]: - type Id = Ident - type NotFound = FactoryError.EntityNotFound[Id] - type AlreadyExists = FactoryError.EntityAlreadyExists[Id] - - protected def AlreadyExists(id: Id): AlreadyExists = - FactoryError.EntityAlreadyExists(entityId, id) - - protected def NotFound(id: Id): NotFound = - FactoryError.EntityNotFound(entityId, id) - - def entityId: String - def make(id: Id): IO[AlreadyExists, T] - def load(id: Id): IO[NotFound, T] - - def loadOrMake(id: Id): UIO[T] = - load(id) - .orElse(make(id)) - .orDieWith(_ => - new RuntimeException( - s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" - ) - ) - - def make()(using idGen: IdGenerator[Id]): UIO[T] = - val tryToCreate = for - id <- idGen.nextId - entity <- make(id) - yield entity - - tryToCreate - .retryN(10) - .orDieWith(_ => - new RuntimeException( - "Cannot create entity, generator failed 10 times to create new ID" - ) - ) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala deleted file mode 100644 index b2fef3a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityFactory.scala +++ /dev/null @@ -1,24 +0,0 @@ -package works.iterative.entity - -import works.iterative.core.Validated -import works.iterative.core.UserMessage -import works.iterative.core.service.IdGenerator -import zio.* - -/** Generic class for entity factories. - * - * Creates a new entity from a "seed". - */ -trait EntityFactory[Entity, Seed]: - def make(seed: Seed): IO[UserMessage, Entity] - -/** A factory using idGenerator to create the entity */ -class GeneratingFactory[Entity, Seed, Id]( - constructor: Seed => Validated[Id => Entity], - idGenerator: IdGenerator[Id] -) extends EntityFactory[Entity, Seed]: - override def make(seed: Seed): IO[UserMessage, Entity] = - for - construct <- constructor(seed).toZIO - id <- idGenerator.nextId - yield construct(id) diff --git a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala b/core/shared/src/main/scala/works/iterative/entity/EntityService.scala deleted file mode 100644 index 29ae384..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/EntityService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package works.iterative.entity - -import zio.* -import works.iterative.core.auth.CurrentUser - -trait EntityUpdateService[Id, Command, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def update(id: Id, command: Command): Op[Unit] - -trait EntityCreateService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(initData: Init): Op[Id] - -trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: - type Op[A] = ZIO[CurrentUser, Error, A] - - def create(id: Id, initData: Init): Op[Id] - -trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] - extends EntityCreateService[Id, Init, Error] - with EntityUpdateService[Id, Command, Error] diff --git a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala deleted file mode 100644 index 946011a..0000000 --- a/core/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala +++ /dev/null @@ -1,6 +0,0 @@ -package works.iterative.entity - -import zio.* - -trait ViewProcessor[Event]: - def process(event: Event): UIO[Unit] diff --git a/core/shared/src/main/scala/works/iterative/event/Event.scala b/core/shared/src/main/scala/works/iterative/event/Event.scala deleted file mode 100644 index 7907ac0..0000000 --- a/core/shared/src/main/scala/works/iterative/event/Event.scala +++ /dev/null @@ -1,4 +0,0 @@ -package works.iterative.event - -trait Event[A]: - def record: EventRecord diff --git a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala b/core/shared/src/main/scala/works/iterative/event/EventRecord.scala deleted file mode 100644 index 490417a..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventRecord.scala +++ /dev/null @@ -1,15 +0,0 @@ -package works.iterative.event - -import works.iterative.core.* -import zio.* - -/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ -final case class EventRecord(userHandle: UserHandle, timestamp: Moment): - val displayName: String = userHandle.displayName - -object EventRecord: - def apply(userHandle: UserHandle): UIO[EventRecord] = - Moment.now.map(EventRecord(userHandle, _)) - - def now(using userHandle: UserHandle): UIO[EventRecord] = - apply(userHandle) diff --git a/core/shared/src/main/scala/works/iterative/event/EventStore.scala b/core/shared/src/main/scala/works/iterative/event/EventStore.scala deleted file mode 100644 index 3226df3..0000000 --- a/core/shared/src/main/scala/works/iterative/event/EventStore.scala +++ /dev/null @@ -1,13 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Abstraction of event log - * - * The log can persist events and restore the state of an entity from the - * events. - */ -trait EventStore[Id, T <: Event[_]]: - type Op[A] = UIO[A] - def persist(id: Id, event: T): Op[Unit] - def get(id: Id): Op[Seq[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala deleted file mode 100644 index 6ccccd7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/SnapshotStore.scala +++ /dev/null @@ -1,12 +0,0 @@ -package works.iterative.event - -import zio.* - -/** Snapshot store - * - * Stores a snapshot of an entity. - */ -trait SnapshotStore[Id, T]: - type Op[A] = UIO[A] - def update(id: Id, snapshot: T): Op[Unit] - def get(id: Id): Op[Option[T]] diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala deleted file mode 100644 index 964d420..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala +++ /dev/null @@ -1,21 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) - extends EventStore[Id, T]: - override def persist(id: Id, event: T): Op[Unit] = - data.update { m => - val events = m.getOrElse(id, Nil) - m.updated(id, event :: events) - }.unit - override def get(id: Id): Op[Seq[T]] = - data.get.map(_.getOrElse(id, Nil)) - -object InMemoryEventStore: - def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, List[T]]) - yield InMemoryEventStore(data) - } diff --git a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala deleted file mode 100644 index 722d7d7..0000000 --- a/core/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala +++ /dev/null @@ -1,19 +0,0 @@ -package works.iterative.event -package impl - -import zio.* - -class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) - extends SnapshotStore[Id, T]: - override def update(id: Id, snapshot: T): UIO[Unit] = - data.update(_.updated(id, snapshot)).unit - - override def get(id: Id): UIO[Option[T]] = - data.get.map(_.get(id)) - -object InMemorySnapshotStore: - def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = - ZLayer { - for data <- Ref.make(Map.empty[Id, T]) - yield InMemorySnapshotStore(data) - } diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala new file mode 100644 index 0000000..9b8f18c --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateError.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import works.iterative.core.UserMessage + +trait AggregateError: + def userMessage: UserMessage diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala new file mode 100644 index 0000000..65c7e92 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRoot.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* + +/** Represents aggregate root in DDD + * + * @tparam Id + * Aggregate root id + * @tparam Command + * Aggregate root command + * @tparam Event + * Aggregate root event + * @tparam State + * Aggregate root state + */ +trait AggregateRoot[Id, Error <: AggregateError, Command, Event, State]: + /** Aggregate root id */ + def id: Id + + /** Current state */ + def state: UIO[State] + + def handle(command: Command): IO[Error, Unit] diff --git a/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala new file mode 100644 index 0000000..c051650 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/AggregateRootFactory.scala @@ -0,0 +1,58 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator + +sealed trait FactoryError: + def entityId: String + def userMessage: UserMessage + +object FactoryError: + case class EntityAlreadyExists[Id](entityId: String, id: Id) + extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.exists", id) + + case class EntityNotFound[Id](entityId: String, id: Id) extends FactoryError: + def userMessage = UserMessage(s"${entityId}.error.entity.not.found", id) + +trait AggregateRootFactory[ + Ident, + T <: AggregateRoot[Ident, ?, ?, ?, ?] +]: + type Id = Ident + type NotFound = FactoryError.EntityNotFound[Id] + type AlreadyExists = FactoryError.EntityAlreadyExists[Id] + + protected def AlreadyExists(id: Id): AlreadyExists = + FactoryError.EntityAlreadyExists(entityId, id) + + protected def NotFound(id: Id): NotFound = + FactoryError.EntityNotFound(entityId, id) + + def entityId: String + def make(id: Id): IO[AlreadyExists, T] + def load(id: Id): IO[NotFound, T] + + def loadOrMake(id: Id): UIO[T] = + load(id) + .orElse(make(id)) + .orDieWith(_ => + new RuntimeException( + s"Loading or making entity ${entityId} with id ${id} failed, loading fails with NotFound, making fails with AlreadyExists" + ) + ) + + def make()(using idGen: IdGenerator[Id]): UIO[T] = + val tryToCreate = for + id <- idGen.nextId + entity <- make(id) + yield entity + + tryToCreate + .retryN(10) + .orDieWith(_ => + new RuntimeException( + "Cannot create entity, generator failed 10 times to create new ID" + ) + ) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala new file mode 100644 index 0000000..b2fef3a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityFactory.scala @@ -0,0 +1,24 @@ +package works.iterative.entity + +import works.iterative.core.Validated +import works.iterative.core.UserMessage +import works.iterative.core.service.IdGenerator +import zio.* + +/** Generic class for entity factories. + * + * Creates a new entity from a "seed". + */ +trait EntityFactory[Entity, Seed]: + def make(seed: Seed): IO[UserMessage, Entity] + +/** A factory using idGenerator to create the entity */ +class GeneratingFactory[Entity, Seed, Id]( + constructor: Seed => Validated[Id => Entity], + idGenerator: IdGenerator[Id] +) extends EntityFactory[Entity, Seed]: + override def make(seed: Seed): IO[UserMessage, Entity] = + for + construct <- constructor(seed).toZIO + id <- idGenerator.nextId + yield construct(id) diff --git a/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala new file mode 100644 index 0000000..29ae384 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/EntityService.scala @@ -0,0 +1,23 @@ +package works.iterative.entity + +import zio.* +import works.iterative.core.auth.CurrentUser + +trait EntityUpdateService[Id, Command, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def update(id: Id, command: Command): Op[Unit] + +trait EntityCreateService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(initData: Init): Op[Id] + +trait EntityCreateWithIdService[Id, Init, Error <: AggregateError]: + type Op[A] = ZIO[CurrentUser, Error, A] + + def create(id: Id, initData: Init): Op[Id] + +trait EntityService[Id, Command, Error <: AggregateError, Init <: Command] + extends EntityCreateService[Id, Init, Error] + with EntityUpdateService[Id, Command, Error] diff --git a/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala new file mode 100644 index 0000000..946011a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/entity/ViewProcessor.scala @@ -0,0 +1,6 @@ +package works.iterative.entity + +import zio.* + +trait ViewProcessor[Event]: + def process(event: Event): UIO[Unit] diff --git a/entity/shared/src/main/scala/works/iterative/event/Event.scala b/entity/shared/src/main/scala/works/iterative/event/Event.scala new file mode 100644 index 0000000..7907ac0 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/Event.scala @@ -0,0 +1,4 @@ +package works.iterative.event + +trait Event[A]: + def record: EventRecord diff --git a/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala new file mode 100644 index 0000000..490417a --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventRecord.scala @@ -0,0 +1,15 @@ +package works.iterative.event + +import works.iterative.core.* +import zio.* + +/** A simple holder for _who_ did _when_, meant to be bound to the _what_ */ +final case class EventRecord(userHandle: UserHandle, timestamp: Moment): + val displayName: String = userHandle.displayName + +object EventRecord: + def apply(userHandle: UserHandle): UIO[EventRecord] = + Moment.now.map(EventRecord(userHandle, _)) + + def now(using userHandle: UserHandle): UIO[EventRecord] = + apply(userHandle) diff --git a/entity/shared/src/main/scala/works/iterative/event/EventStore.scala b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala new file mode 100644 index 0000000..3226df3 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/EventStore.scala @@ -0,0 +1,13 @@ +package works.iterative.event + +import zio.* + +/** Abstraction of event log + * + * The log can persist events and restore the state of an entity from the + * events. + */ +trait EventStore[Id, T <: Event[_]]: + type Op[A] = UIO[A] + def persist(id: Id, event: T): Op[Unit] + def get(id: Id): Op[Seq[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala new file mode 100644 index 0000000..6ccccd7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/SnapshotStore.scala @@ -0,0 +1,12 @@ +package works.iterative.event + +import zio.* + +/** Snapshot store + * + * Stores a snapshot of an entity. + */ +trait SnapshotStore[Id, T]: + type Op[A] = UIO[A] + def update(id: Id, snapshot: T): Op[Unit] + def get(id: Id): Op[Option[T]] diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala new file mode 100644 index 0000000..964d420 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemoryEventStore.scala @@ -0,0 +1,21 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemoryEventStore[Id, T <: Event[_]](data: Ref[Map[Id, List[T]]]) + extends EventStore[Id, T]: + override def persist(id: Id, event: T): Op[Unit] = + data.update { m => + val events = m.getOrElse(id, Nil) + m.updated(id, event :: events) + }.unit + override def get(id: Id): Op[Seq[T]] = + data.get.map(_.getOrElse(id, Nil)) + +object InMemoryEventStore: + def layer[Id: Tag, T <: Event[_]: Tag]: ULayer[EventStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, List[T]]) + yield InMemoryEventStore(data) + } diff --git a/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala new file mode 100644 index 0000000..722d7d7 --- /dev/null +++ b/entity/shared/src/main/scala/works/iterative/event/impl/InMemorySnapshotStore.scala @@ -0,0 +1,19 @@ +package works.iterative.event +package impl + +import zio.* + +class InMemorySnapshotStore[Id, T](data: Ref[Map[Id, T]]) + extends SnapshotStore[Id, T]: + override def update(id: Id, snapshot: T): UIO[Unit] = + data.update(_.updated(id, snapshot)).unit + + override def get(id: Id): UIO[Option[T]] = + data.get.map(_.get(id)) + +object InMemorySnapshotStore: + def layer[Id: Tag, T: Tag](): ULayer[SnapshotStore[Id, T]] = + ZLayer { + for data <- Ref.make(Map.empty[Id, T]) + yield InMemorySnapshotStore(data) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala new file mode 100644 index 0000000..874d16c --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/ConsulKeyValueStore.scala @@ -0,0 +1,63 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import sttp.client3.* + +class ConsulKeyValueStore( + token: ConsulToken, + baseUri: BaseUri, + backend: CustomTapir.Backend +) extends KeyValueStore[String, String]: + + private val defaultBase = "http://localhost:8500" + private val base = baseUri.value.map(_.toString).getOrElse(defaultBase) + + private def addAuth[U[_], T, R]( + request: RequestT[U, T, R] + ): RequestT[U, T, R] = + request.header("X-Consul-Token", token.token) + + override def get(key: String): UIO[Option[String]] = { + for + response <- addAuth(basicRequest.get(uri"$base/v1/kv/${key}?raw=true")) + .send(backend) + yield response.body.toOption + }.orDie + + override def put(key: String, value: String): UIO[Unit] = { + addAuth(basicRequest.put(uri"$base/v1/kv/${key}").body(value)) + .send(backend) + .unit + }.orDie + + override def remove(key: String): UIO[Unit] = { + addAuth(basicRequest.delete(uri"$base/v1/kv/${key}")) + .send(backend) + .unit + }.orDie + +object ConsulKeyValueStore: + val layer: URLayer[ + ConsulToken & BaseUri & CustomTapir.Backend, + StringKeyValueStore + ] = + ZLayer { + for + token <- ZIO.service[ConsulToken] + baseUri <- ZIO.service[BaseUri] + backend <- ZIO.service[CustomTapir.Backend] + yield ConsulKeyValueStore(token, baseUri, backend) + } + + val fromEnv + : ZLayer[CustomTapir.Backend, SecurityException, StringKeyValueStore] = + ConsulToken.fromEnv >>> ZLayer { + for + token <- ZIO.service[ConsulToken] + backend <- ZIO.service[CustomTapir.Backend] + addr <- System.envOrElse("CONSUL_ADDR", "http://localhost:8500") + yield ConsulKeyValueStore(token, BaseUri(addr), backend) + } diff --git a/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala new file mode 100644 index 0000000..5c5483a --- /dev/null +++ b/hashicorp/jvm/src/main/scala/works/iterative/core/service/impl/endpoints.scala @@ -0,0 +1,150 @@ +package works.iterative.core.service.impl + +import zio.* +import sttp.tapir.EndpointIO.annotations.* +import sttp.tapir.Schema +import zio.json.* + +final case class ConsulToken(token: Option[String]) + +object ConsulToken: + def fromEnv: ZLayer[Any, SecurityException, ConsulToken] = ZLayer { + System.env("CONSUL_TOKEN").map(ConsulToken(_)) + } + +@jsonMemberNames(PascalCase) +final case class ConsulMetadata( + createIndex: Long, + modifyIndex: Long, + lockIndex: Long, + key: String, + flags: Long, + value: String, + session: Option[String] +) derives JsonCodec, + Schema + +@endpointInput("v1/kv/{key}") +final case class GetQuery( + @path + @description("Key to get from the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + // TODO: This would not work type-wise properly, as the result type of the call would differ depending on the query parameters + // These are missing, split to different queries to enable. + /* + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies the response is just the raw value of the key, without any encoding or metadata." + ) + raw: Option[Boolean] = None, + @query + @description( + "Specifies to return only keys (no values or metadata). Specifying this parameter implies recurse." + ) + keys: Option[Boolean] = None, + @query + @description( + "Specifies the string to use as a separator for recursive key lookups. This option is only used when paired with the keys parameter to limit the prefix of keys returned, only up to the given separator." + ) + separator: Option[String] = None, + */ + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class PutQuery( + @path + @description("Key to put into the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies an unsigned value between 0 and (2^64)-1 to store with the key. API consumers can use this field any way they choose for their application." + ) + flags: Option[Long] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Supply a session ID to use in a lock acquisition operation.") + acquire: Option[String] = None, + @query + @description("Supply a session ID to use in a release operation.") + release: Option[String] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +@endpointInput("v1/kv/{key}") +final case class DeleteQuery( + @path + @description("Key to delete in the KV store") + key: String, + @query + @description("Datacenter to query") + dc: Option[String] = None, + @query + @description( + "Specifies if the lookup should be recursive and treat key as a prefix instead of a literal match." + ) + recurse: Option[Boolean] = None, + @query + @description( + "Specifies to use a Check-And-Set operation. This is very useful as a building block for more complex synchronization primitives. If the index is 0, Consul will only put the key if it does not already exist. If the index is non-zero, the key is only set if the index matches the ModifyIndex of that key." + ) + cas: Option[Long] = None, + @query + @description("Enterprise only. Specifies the namespace to query.") + ns: Option[String] = None +) + +/* +sealed trait ConsulGetError +object ConsulGetError: + case object NotFound extends ConsulGetError derives JsonCodec, Schema + case object Forbidden extends ConsulGetError derives JsonCodec, Schema + case class ServerError(msg: String) extends ConsulGetError derives JsonCodec, Schema + +trait ConsulKVEndpoints extends CustomTapir: + private val kvBase = endpoint + .in("v1" / "kv") + .securityIn( + auth.apiKey(header[Option[String]]("X-Consul-Token").mapTo[ConsulToken]) + ) + + val getKV: Endpoint[ConsulToken, GetQuery, Unit, ConsulMetadata, Any] = + kvBase.get + .in(EndpointInput.derived[GetQuery]) + .out(jsonBody[ConsulMetadata]) + .errorOut( + oneOf[ConsulGetError]( + oneOfVariant( + statusCode(StatusCode.NotFound).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfVariant( + statusCode(StatusCode.Forbidden).and(jsonBody[ConsulGetError.NotFound.type].description("not found")) + ),oneOfDefaultVariant( + statusCode(StatusCode.InternalServerError).and(jsonBody[ServerError].description("not found")) + ) + ) + ) + val putKV: Endpoint[ConsulToken, PutQuery, Unit, Boolean, Any] = + kvBase.put.in(EndpointInput.derived[PutQuery]).out(jsonBody[Boolean]) + val deleteKV: Endpoint[ConsulToken, DeleteQuery, Unit, Boolean, Any] = + kvBase.delete.in(EndpointInput.derived[DeleteQuery]).out(jsonBody[Boolean]) + +*/ \ No newline at end of file diff --git a/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala new file mode 100644 index 0000000..c938083 --- /dev/null +++ b/hashicorp/jvm/src/test/scala/works/iterative/core/service/impl/ConsulKeyValueStoreSpec.scala @@ -0,0 +1,19 @@ +package works.iterative.core +package service +package impl + +import works.iterative.tapir.{BaseUri, CustomTapir} +import zio.* +import zio.test.* + +object ConsulKeyValueStoreSpec extends specs.KeyValueStoreSpec: + override def spec = defaultSpec.provide( + ConsulKeyValueStore.layer, + CustomTapir.clientLayer, + ZLayer { + Live.live(System.env("CONSUL_ADDR")).map(a => BaseUri(a.get)) + }, + ZLayer { + Live.live(System.env("CONSUL_TOKEN")).map(ConsulToken(_)) + } + ) @@ TestAspect.ifEnvSet("CONSUL_ADDR") diff --git a/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala b/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala new file mode 100644 index 0000000..16211a8 --- /dev/null +++ b/service/js/src/main/scala/works/iterative/core/service/impl/JsStorageRepository.scala @@ -0,0 +1,29 @@ +package works.iterative.core.service.impl + +import org.scalajs.dom.Storage +import works.iterative.core.service.Repository +import zio.* +import zio.json.* + +// TODO: improve error reporting on generic repositories +// This is good just for prototypes +// And it cannot be used to query data, as there is no way to iterate the storage +class JsStorageRepository[Value: JsonCodec]( + storage: Storage +) extends Repository[String, Value, String]: + + override def load(id: String): UIO[Option[Value]] = { + for + raw <- ZIO.attemptBlocking(Option(storage.getItem(id))) + data <- ZIO.foreach(raw) { r => + ZIO + .fromEither(r.fromJson[Value]) + .mapError(_ => new RuntimeException(s"Failed to parse: ${raw}")) + } + yield data + }.orDie + + override def save(key: String, value: Value): UIO[Unit] = + ZIO.attemptBlocking(storage.setItem(key, value.toJson)).orDie + + override def find(id: String): UIO[List[Value]] = load(id).map(_.toList) diff --git a/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala b/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala new file mode 100644 index 0000000..8c88818 --- /dev/null +++ b/service/jvm/src/main/scala/works/iterative/core/service/impl/JcaDigestService.scala @@ -0,0 +1,26 @@ +package works.iterative.core +package service +package impl + +import zio.* +import zio.stream.* + +class JcaDigestService extends DigestService: + + override def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): IO[E, Digest] = + for + md <- ZIO + .attempt( + java.security.MessageDigest.getInstance(algorithm.value) + ) + .orDie + _ <- content.runForeachChunk(e => ZIO.attempt(md.update(e.toArray)).orDie) + digest <- ZIO.attempt(md.digest()).orDie + yield Digest(algorithm, digest) + +object JcaDigestGenerator: + val layer: ULayer[DigestService] = + ZLayer.succeed(new JcaDigestService) diff --git a/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala b/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala new file mode 100644 index 0000000..883bd7a --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/DigestService.scala @@ -0,0 +1,26 @@ +package works.iterative.core.service + +import works.iterative.core.Digest +import works.iterative.core.DigestAlgorithm + +import zio.* +import zio.stream.* + +trait DigestService: + def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): IO[E, Digest] + +object DigestService: + def digest[E]( + algorithm: DigestAlgorithm, + content: Stream[E, Byte] + ): ZIO[DigestService, E, Digest] = + ZIO.serviceWithZIO[DigestService](_.digest(algorithm, content)) + + def digest( + algorithm: DigestAlgorithm, + content: Array[Byte] + ): URIO[DigestService, Digest] = + digest(algorithm, ZStream.fromChunk(Chunk.fromArray(content))) diff --git a/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala b/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala new file mode 100644 index 0000000..10bdeb7 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/FileStore.scala @@ -0,0 +1,29 @@ +package works.iterative.core.service + +import works.iterative.core.FileSupport.* +import works.iterative.core.FileRef + +import zio.* + +trait FileStore: + type Op[A] = UIO[A] + def store(file: FileRepr): Op[FileRef] + def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] + def load(url: String): Op[Option[Array[Byte]]] + +object FileStore: + type Op[A] = URIO[FileStore, A] + def store(file: FileRepr): Op[FileRef] = + ZIO.serviceWithZIO(_.store(file)) + def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] = + ZIO.serviceWithZIO(_.store(name, file, contentType)) + def load(url: String): Op[Option[Array[Byte]]] = + ZIO.serviceWithZIO(_.load(url)) diff --git a/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala b/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala new file mode 100644 index 0000000..1779da3 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/IdGenerator.scala @@ -0,0 +1,10 @@ +package works.iterative.core.service + +import zio.* + +/** Generator of unique IDs of a given type */ +trait IdGenerator[A]: + self => + def nextId: UIO[A] + def map[B](f: A => B): IdGenerator[B] = new IdGenerator[B]: + def nextId: UIO[B] = self.nextId.map(f) diff --git a/service/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala b/service/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala new file mode 100644 index 0000000..23d1908 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/KeyValueStore.scala @@ -0,0 +1,47 @@ +package works.iterative.core.service + +import zio.* +import zio.json.* + +trait GenericReadKeyValueStore[Eff[+_], -Key, +Value]: + type Op[A] = Eff[A] + def get(key: Key): Eff[Option[Value]] + +trait GenericWriteKeyValueStore[Eff[+_], -Key, -Value]: + def put(key: Key, value: Value): Eff[Unit] + def remove(key: Key): Eff[Unit] + +trait GenericKeyValueStore[Eff[+_], -Key, Value] + extends GenericReadKeyValueStore[Eff, Key, Value] + with GenericWriteKeyValueStore[Eff, Key, Value] + +trait ReadKeyValueStore[-Key, +Value] + extends GenericReadKeyValueStore[UIO, Key, Value] + +trait WriteKeyValueStore[-Key, -Value] + extends GenericWriteKeyValueStore[UIO, Key, Value] + +trait KeyValueStore[-Key, Value] + extends GenericKeyValueStore[UIO, Key, Value] + with ReadKeyValueStore[Key, Value] + with WriteKeyValueStore[Key, Value] + +type StringKeyValueStore = KeyValueStore[String, String] + +object KeyValueStore: + extension (store: StringKeyValueStore) + /** Decode the value, ignoring decoding errors if any */ + def getAsMaybe[A: JsonDecoder](key: String): UIO[Option[A]] = + store.get(key).map(_.flatMap(_.fromJson[A].toOption)) + + def getAsMaybeLogged[A: JsonDecoder](key: String): UIO[Option[A]] = + store.get(key).flatMap { + case None => ZIO.none + case Some(v) => + ZIO.fromEither(v.fromJson[A]).asSome.catchAll { err => + ZIO.log(err) *> ZIO.none + } + } + + def putAsJson[A: JsonEncoder](key: String, value: A): UIO[Unit] = + store.put(key, value.toJson) diff --git a/service/shared/src/main/scala/works/iterative/core/service/Repository.scala b/service/shared/src/main/scala/works/iterative/core/service/Repository.scala new file mode 100644 index 0000000..c450279 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/Repository.scala @@ -0,0 +1,52 @@ +package works.iterative.core.service + +import zio.* +import zio.stream.* + +trait GenericLoadService[Eff[+_], -Key, +Value]: + type Op[A] = Eff[A] + def load(id: Key): Op[Option[Value]] + +trait GenericUpdateNotifyService[Eff[+_], Str[+_], Key]: + def updates: Eff[Str[Key]] + +trait GenericLoadAllService[Eff[+_], Coll[+_], -Key, +Value]: + type Op[A] = Eff[A] + def loadAll(ids: Seq[Key]): Op[Coll[Value]] + +trait GenericFindService[Eff[+_], Coll[+_], -Key, +Value, -FilterArg]: + type Op[A] = Eff[A] + def find(filter: FilterArg): Op[Coll[Value]] + +trait GenericReadRepository[Eff[+_], Coll[+_], -Key, +Value, -FilterArg] + extends GenericLoadService[Eff, Key, Value] + with GenericLoadAllService[Eff, Coll, Key, Value] + with GenericFindService[Eff, Coll, Key, Value, FilterArg] + +trait GenericWriteRepository[Eff[_], -Key, -Value]: + type Op[A] = Eff[A] + def save(key: Key, value: Value): Op[Unit] + +trait GenericRepository[Eff[+_], -Key, Value] + extends GenericReadRepository[Eff, List, Key, Value, Unit] + with GenericWriteRepository[Eff, Key, Value] + +type LoadRepository[-Key, +Value] = GenericLoadService[UIO, Key, Value] +type LoadAllRepository[-Key, +Value] = + GenericLoadAllService[UIO, List, Key, Value] + +trait ReadRepository[-Key, +Value, -FilterArg] + extends GenericReadRepository[UIO, List, Key, Value, FilterArg]: + override def loadAll(ids: Seq[Key]): UIO[List[Value]] = + // Inefficient implementation, meant to be overridden + ZIO.foreach(ids)(load).map(_.flatten.toList) + +trait UpdateNotifyRepository[Key] + extends GenericUpdateNotifyService[UIO, UStream, Key] + +trait WriteRepository[-Key, -Value] + extends GenericWriteRepository[UIO, Key, Value] + +trait Repository[-Key, Value, -FilterArg] + extends ReadRepository[Key, Value, FilterArg] + with WriteRepository[Key, Value] diff --git a/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala b/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala new file mode 100644 index 0000000..66ad66b --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryFileStore.scala @@ -0,0 +1,21 @@ +package works.iterative.core.service +package impl + +import works.iterative.core.FileSupport.* +import works.iterative.core.FileRef +import zio.* + +object InMemoryFileStore: + val layer: ULayer[FileStore] = ZLayer.succeed { + new FileStore: + override def store(file: FileRepr): Op[FileRef] = + ZIO.succeed(FileRef.unsafe(file.name, "#")) + override def store( + name: String, + file: Array[Byte], + contentType: Option[String] + ): Op[FileRef] = + ZIO.succeed(FileRef.unsafe(name, "#")) + override def load(url: String): Op[Option[Array[Byte]]] = + ZIO.succeed(None) + } diff --git a/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala b/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala new file mode 100644 index 0000000..e3b7d45 --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/impl/InMemoryRepository.scala @@ -0,0 +1,17 @@ +package works.iterative.core.service +package impl + +import zio.* + +trait InMemoryRepository[Key, Value, FilterArg]( + data: Ref[Map[Key, Value]], + filter: FilterArg => Value => Boolean +) extends Repository[Key, Value, FilterArg]: + override def save(key: Key, value: Value): UIO[Unit] = + data.update(_ + (key -> value)) + override def load(key: Key): UIO[Option[Value]] = + data.get.map(_.get(key)) + override def loadAll(keys: Seq[Key]): UIO[List[Value]] = + data.get.map(_.view.filterKeys(keys.contains).values.toList) + override def find(filterArg: FilterArg): UIO[List[Value]] = + data.get.map(_.values.filter(filter(filterArg)).toList) diff --git a/service/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala b/service/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala new file mode 100644 index 0000000..358cb9d --- /dev/null +++ b/service/shared/src/main/scala/works/iterative/core/service/impl/UUIDGenerator.scala @@ -0,0 +1,11 @@ +package works.iterative.core.service.impl + +import works.iterative.core.service.IdGenerator +import zio.* + +class UUIDGenerator[A](f: String => A) extends IdGenerator[A]: + def nextId: UIO[A] = Random.nextUUID.map(v => f(v.toString)) + +object UUIDGenerator: + def layer[A: Tag](f: String => A): ULayer[IdGenerator[A]] = + ZLayer.succeed(UUIDGenerator(f)) diff --git a/service/specs/shared/src/main/scala/works/iterative/core/service/specs/KeyValueStoreSpec.scala b/service/specs/shared/src/main/scala/works/iterative/core/service/specs/KeyValueStoreSpec.scala new file mode 100644 index 0000000..0548a3f --- /dev/null +++ b/service/specs/shared/src/main/scala/works/iterative/core/service/specs/KeyValueStoreSpec.scala @@ -0,0 +1,34 @@ +package works.iterative.core +package service +package specs + +import zio.* +import zio.test.* + +abstract class KeyValueStoreSpec extends ZIOSpecDefault: + val defaultSpec = suite("Consul spec")( + test("get returns None if key does not exist") { + for + kv <- ZIO.service[StringKeyValueStore] + uuid <- Random.nextUUID + key = s"test/$uuid" + value <- kv.get(key) + yield assertTrue(value.isEmpty) + }, + test("read/put/delete works") { + for + kv <- ZIO.service[StringKeyValueStore] + uuid <- Random.nextUUID + key = s"test/$uuid" + originalValue <- kv.get(key) + _ <- kv.put(key, "test_value") + updatedValue <- kv.get(key) + _ <- kv.remove(key) + removedValue <- kv.get(key) + yield assertTrue( + originalValue.isEmpty, + updatedValue.contains("test_value"), + removedValue.isEmpty + ) + } + ) diff --git a/tapir/shared/src/main/scala/works/iterative/tapir/ClientEndpointFactory.scala b/tapir/shared/src/main/scala/works/iterative/tapir/ClientEndpointFactory.scala index f232c35..d9d8b30 100644 --- a/tapir/shared/src/main/scala/works/iterative/tapir/ClientEndpointFactory.scala +++ b/tapir/shared/src/main/scala/works/iterative/tapir/ClientEndpointFactory.scala @@ -1,10 +1,12 @@ package works.iterative.tapir import zio.* -import sttp.tapir.PublicEndpoint +import sttp.tapir.Endpoint import sttp.capabilities.zio.ZioStreams import sttp.capabilities.WebSockets +import scala.compiletime.{erasedValue, summonFrom} + opaque type Client[I, E, O] = I => IO[E, O] object Client: @@ -14,26 +16,59 @@ def apply(i: I): IO[E, O] = f(i) def toEffect: I => ZIO[Any, E, O] = i => f(i) +type ClientError[E] = E match + case Unit => Nothing + case _ => E + +object ClientError: + inline def cause[E](e: Cause[E]): Cause[ClientError[E]] = + erasedValue[E] match + case _: Unit => + Cause.die(throw new IllegalStateException("Internal Server Error")) + case _ => e.asInstanceOf[Cause[ClientError[E]]] + + inline def apply[S, I, E, A]( + client: SecureClient[S, I, E, A] + ): SecureClient[S, I, ClientError[E], A] = + s => i => client(s)(i).mapErrorCause(cause(_)) + +opaque type SecureClient[S, I, E, O] = S => I => IO[E, O] + +object SecureClient: + def apply[S, I, E, O](f: S => I => IO[E, O]): SecureClient[S, I, E, O] = f + + extension [S, I, E, O](f: SecureClient[S, I, E, O]) + def apply(s: S): Client[I, E, O] = f(s) + def toEffect: S => I => ZIO[Any, E, O] = s => i => f(s)(i) + +type ClientResult[S, I, E, O] = S match + case Unit => I => IO[ClientError[E], O] + case _ => S => I => IO[ClientError[E], O] + +/** Create effectful methods to perform the endpoint operation + * + * Just a useful way to have something that will derive the client from the + * endpoint using other layers, like BaseUri and provided STTP Backend. + */ trait ClientEndpointFactory: - def umake[I, O]( - endpoint: PublicEndpoint[I, Unit, O, Any] - ): Client[I, Nothing, O] - def make[I, E, O]( - endpoint: PublicEndpoint[I, E, O, Any] - ): Client[I, E, O] - def stream[I, E, O]( - endpoint: PublicEndpoint[ - Unit, - E, - ZioStreams.Pipe[I, O], - ZioStreams & WebSockets - ] - ): Client[Unit, E, ZioStreams.Pipe[I, O]] - def ustream[I, O]( - endpoint: PublicEndpoint[ - Unit, - Unit, - ZioStreams.Pipe[I, O], - ZioStreams & WebSockets - ] - ): Client[Unit, Nothing, ZioStreams.Pipe[I, O]] + inline def makeSecure[S, I, E, O]( + endpoint: Endpoint[S, I, E, O, Any] + ): S => I => IO[ClientError[E], O] = + inline val isWebSocket = + erasedValue[O] match + case _: ZioStreams.Pipe[I, O] => true + case _ => false + + s => i => makeSecureClient(endpoint, isWebSocket)(s)(i).mapErrorCause(ClientError.cause(_)) + + transparent inline def make[S, I, E, O]( + endpoint: Endpoint[S, I, E, O, Any] + ): ClientResult[S, I, E, O] = + erasedValue[S] match + case _: Unit => makeSecure(endpoint)(().asInstanceOf[S]) + case _ => makeSecure(endpoint) + + def makeSecureClient[S, I, E, O]( + endpoint: Endpoint[S, I, E, O, ZioStreams & WebSockets], + isWebSocket: Boolean = false + ): S => I => IO[E, O] diff --git a/tapir/shared/src/main/scala/works/iterative/tapir/LiveClientEndpointFactory.scala b/tapir/shared/src/main/scala/works/iterative/tapir/LiveClientEndpointFactory.scala index 7657048..58de66c 100644 --- a/tapir/shared/src/main/scala/works/iterative/tapir/LiveClientEndpointFactory.scala +++ b/tapir/shared/src/main/scala/works/iterative/tapir/LiveClientEndpointFactory.scala @@ -1,7 +1,7 @@ package works.iterative.tapir import zio.* -import sttp.tapir.PublicEndpoint +import sttp.tapir.Endpoint import sttp.capabilities.zio.ZioStreams import sttp.capabilities.WebSockets import sttp.tapir.client.sttp.ws.zio.* @@ -12,49 +12,11 @@ ) extends ClientEndpointFactory with CustomTapir: - override def umake[I, O]( - endpoint: PublicEndpoint[I, Unit, O, Any] - ): Client[I, Nothing, O] = Client((input: I) => - mkClient(endpoint)(input).orDieWith(_ => - new IllegalStateException("Internal Server Error") - ) - ) - - override def make[I, E, O]( - endpoint: PublicEndpoint[I, E, O, Any] - ): Client[I, E, O] = mkClient(endpoint) - - override def stream[I, E, O]( - endpoint: PublicEndpoint[ - Unit, - E, - ZioStreams.Pipe[I, O], - ZioStreams & WebSockets - ] - ): Client[Unit, E, ZioStreams.Pipe[I, O]] = - mkClient(endpoint, true) - - override def ustream[I, O]( - endpoint: PublicEndpoint[ - Unit, - Unit, - ZioStreams.Pipe[I, O], - ZioStreams & WebSockets - ] - ): Client[Unit, Nothing, ZioStreams.Pipe[I, O]] = Client( - mkClient(endpoint, true)(_).orDieWith(_ => - new IllegalStateException("Internal Server Error") - ) - ) - - private def mkClient[I, E, O]( - endpoint: PublicEndpoint[I, E, O, ZioStreams & WebSockets], + override def makeSecureClient[S, I, E, O]( + endpoint: Endpoint[S, I, E, O, ZioStreams & WebSockets], isWebSocket: Boolean = false - )(using - baseUri: BaseUri, - backend: Backend - ): Client[I, E, O] = Client((input: I) => - val req = toRequest( + ): S => I => IO[E, O] = (securityInput: S) => (input: I) => + val req = toSecureRequest( endpoint, if isWebSocket then baseUri.toUri.map(b => @@ -64,8 +26,10 @@ ) else baseUri.toUri ) - val fetch = req(input).followRedirects(false).send(backend) - for + + val fetch = req(securityInput)(input).followRedirects(false).send(backend) + + val result = for resp <- fetch.orDie body <- resp.body match case DecodeResult.Value(v) => ZIO.succeed(v) @@ -77,7 +41,8 @@ ) v <- ZIO.fromEither(body) yield v - ) + + result object LiveClientEndpointFactory: val layer: URLayer[BaseUri & CustomTapir.Backend, ClientEndpointFactory] = diff --git a/ui/js/src/main/scala/works/iterative/ui/components/ReloadableComponent.scala b/ui/js/src/main/scala/works/iterative/ui/components/ReloadableComponent.scala index 09030d4..c5aca29 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/ReloadableComponent.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/ReloadableComponent.scala @@ -120,4 +120,10 @@ for given Runtime[Any] <- ZIO.runtime[Any] factory <- ZIO.service[ClientEndpointFactory] - yield new ReloadableComponent(factory.umake(endpoint).toEffect) + yield + val client = factory.makeSecureClient(endpoint)(()) + new ReloadableComponent( + client(_).mapErrorCause(_ => + Cause.die(IllegalStateException("Internal Server Error")) + ) + )