diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml
index 2c2e12c..31f43b1 100644
--- a/.github/workflows/cicd.yml
+++ b/.github/workflows/cicd.yml
@@ -12,7 +12,6 @@ on:
# env variables
env:
- CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
@@ -28,12 +27,9 @@ jobs:
matrix:
# supported scala versions
include:
- - scala: 2.13.12
- name: Scala2_13
- test-tasks: scalafmtCheck test gen-doc
- - scala: 3.3.1
- name: Scala3_3
- test-tasks: test
+ - scala: 3.4.0
+ name: Scala3_4
+ test-tasks: scalafmtCheck gen-doc coverage test coverageReport
steps:
- uses: actions/checkout@v3
@@ -65,8 +61,10 @@ jobs:
run: sbt ++${{ matrix.scala }} mimaReportBinaryIssues
#----------- COVERAGE -----------
- - name: Submit Code Coverage
- run: bash <(curl -s https://codecov.io/bash)
+ - name: Upload coverage reports to Codecov
+ uses: codecov/codecov-action@v3
+ env:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
deploy:
runs-on: ubuntu-latest
diff --git a/.mergify.yml b/.mergify.yml
index b45b4fb..eec5a17 100644
--- a/.mergify.yml
+++ b/.mergify.yml
@@ -4,8 +4,7 @@ pull_request_rules:
- "#approved-reviews-by>=1"
- check-success=codecov/patch
- check-success=codecov/project
- - check-success=build (Scala2_13)
- - check-success=build (Scala3_3)
+ - check-success=build (Scala3_4)
- base=main
- label!=work-in-progress
actions:
@@ -14,8 +13,7 @@ pull_request_rules:
- name: automatic merge for master when CI passes and author is steward
conditions:
- author=scala-steward-geirolz[bot]
- - check-success=build (Scala2_13)
- - check-success=build (Scala3_3)
+ - check-success=build (Scala3_4)
- base=main
actions:
merge:
@@ -23,8 +21,7 @@ pull_request_rules:
- name: automatic merge for master when CI passes and author is dependabot
conditions:
- author=dependabot[bot]
- - check-success=build (Scala2_13)
- - check-success=build (Scala3_3)
+ - check-success=build (Scala3_4)
- base=main
actions:
merge:
diff --git a/.scalafmt.conf b/.scalafmt.conf
index 9c9b14d..a43b7a2 100644
--- a/.scalafmt.conf
+++ b/.scalafmt.conf
@@ -1,6 +1,6 @@
version = 3.7.17
encoding = "UTF-8"
-runner.dialect = "scala213source3"
+runner.dialect = "scala3"
maxColumn = 150
project.git = true
@@ -32,9 +32,3 @@ docstrings.blankFirstLine = no
docstrings.forceBlankLineBefore = true
spaces.inImportCurlyBraces = false
-
-fileOverride {
- "glob:**/scala-3*/**" {
- runner.dialect = scala3
- }
-}
\ No newline at end of file
diff --git a/README.md b/README.md
index 62204c3..572e337 100644
--- a/README.md
+++ b/README.md
@@ -70,31 +70,27 @@ libraryDependencies += "com.github.geirolz" %% "toolkit" % "0.0.11"
```scala
import cats.Show
import cats.effect.{Resource, IO}
-import com.geirolz.app.toolkit.{App, SimpleAppInfo}
-import com.geirolz.app.toolkit.logger.ToolkitLogger
+import com.geirolz.app.toolkit.*
+import com.geirolz.app.toolkit.logger.ConsoleLogger
import com.geirolz.app.toolkit.novalues.NoResources
// Define config
case class Config(host: String, port: Int)
-
-object Config {
- implicit val show: Show[Config] = Show.fromToString
-}
+object Config:
+ given Show[Config] = Show.fromToString
// Define service dependencies
case class AppDependencyServices(kafkaConsumer: KafkaConsumer[IO])
-object AppDependencyServices {
- def resource(res: App.Resources[SimpleAppInfo[String], ToolkitLogger[IO], Config, NoResources]): Resource[IO, AppDependencyServices] =
- Resource.pure(AppDependencyServices(KafkaConsumer.fake))
-}
+object AppDependencyServices:
+ def resource(using AppContext.NoDepsAndRes[SimpleAppInfo[String], ConsoleLogger[IO], Config]): Resource[IO, AppDependencyServices] =
+ Resource.pure(AppDependencyServices(KafkaConsumer.fake))
// A stubbed kafka consumer
-trait KafkaConsumer[F[_]] {
+trait KafkaConsumer[F[_]]:
def consumeFrom(name: String): fs2.Stream[F, KafkaConsumer.KafkaRecord]
-}
-object KafkaConsumer {
+object KafkaConsumer:
import scala.concurrent.duration.DurationInt
@@ -105,7 +101,6 @@ object KafkaConsumer {
fs2.Stream
.eval(IO.randomUUID.map(t => KafkaRecord(t.toString)).flatTap(_ => IO.sleep(5.seconds)))
.repeat
-}
```
3. **Build Your Application:** Build your application using the Toolkit DSL and execute it. Toolkit
@@ -113,35 +108,34 @@ object KafkaConsumer {
```scala
import cats.effect.{ExitCode, IO, IOApp}
-import com.geirolz.app.toolkit.{App, SimpleAppInfo}
-import com.geirolz.app.toolkit.logger.ToolkitLogger
-
-object Main extends IOApp {
- override def run(args: List[String]): IO[ExitCode] =
- App[IO]
- .withInfo(
- SimpleAppInfo.string(
- name = "toolkit",
- version = "0.0.1",
- scalaVersion = "2.13.10",
- sbtVersion = "1.8.0"
+import com.geirolz.app.toolkit.*
+import com.geirolz.app.toolkit.logger.Logger
+
+object Main extends IOApp:
+ override def run(args: List[String]): IO[ExitCode] =
+ App[IO]
+ .withInfo(
+ SimpleAppInfo.string(
+ name = "toolkit",
+ version = "0.0.1",
+ scalaVersion = "2.13.10",
+ sbtVersion = "1.8.0"
+ )
+ )
+ .withConsoleLogger()
+ .withConfigF(IO.pure(Config("localhost", 8080)))
+ .dependsOn(AppDependencyServices.resource)
+ .beforeProviding(ctx.logger.info("CUSTOM PRE-PROVIDING"))
+ .provideOne(
+ // Kafka consumer
+ ctx.dependencies.kafkaConsumer
+ .consumeFrom("test-topic")
+ .evalTap(record => ctx.logger.info(s"Received record $record"))
+ .compile
+ .drain
)
- )
- .withLogger(ToolkitLogger.console[IO](_))
- .withConfigLoader(_ => IO.pure(Config("localhost", 8080)))
- .dependsOn(AppDependencyServices.resource(_))
- .beforeProviding(_.logger.info("CUSTOM PRE-PROVIDING"))
- .provideOne(deps =>
- // Kafka consumer
- deps.dependencies.kafkaConsumer
- .consumeFrom("test-topic")
- .evalTap(record => deps.logger.info(s"Received record $record"))
- .compile
- .drain
- )
- .onFinalize(_.logger.info("CUSTOM END"))
- .run(args)
-}
+ .onFinalize(ctx.logger.info("CUSTOM END"))
+ .run(args)
```
Check a full example [here](https://github.com/geirolz/toolkit/tree/main/examples)
diff --git a/build.sbt b/build.sbt
index ad16903..025d460 100644
--- a/build.sbt
+++ b/build.sbt
@@ -3,9 +3,8 @@ import sbt.project
lazy val prjName = "toolkit"
lazy val prjDescription = "A small toolkit to build functional app with managed resources"
lazy val org = "com.github.geirolz"
-lazy val scala213 = "2.13.12"
-lazy val scala32 = "3.3.1"
-lazy val supportedScalaVersions = List(scala213, scala32)
+lazy val scala34 = "3.4.0"
+lazy val supportedScalaVersions = List(scala34)
//## global project to no publish ##
val copyReadMe = taskKey[Unit]("Copy generated README to main folder.")
@@ -35,23 +34,17 @@ lazy val root: Project = project
.settings(
copyReadMe := IO.copyFile(file("docs/compiled/README.md"), file("README.md"))
)
- .aggregate(core, docs, config, testing, log4cats, odin, pureconfig, fly4s)
+ .aggregate(core, docs, testing, log4cats, odin, pureconfig, fly4s)
lazy val docs: Project =
project
.in(file("docs"))
.enablePlugins(MdocPlugin)
- .dependsOn(core, config, log4cats, odin, pureconfig, fly4s)
+ .dependsOn(core, log4cats, odin, pureconfig, fly4s)
.settings(
baseSettings,
noPublishSettings,
- libraryDependencies ++= Seq(
- CrossVersion.partialVersion(scalaVersion.value) match {
- case Some((2, 13)) => ProjectDependencies.Docs.dedicated_2_13
- case Some((3, _)) => ProjectDependencies.Docs.dedicated_3_2
- case _ => Nil
- }
- ).flatten,
+ libraryDependencies ++= ProjectDependencies.Docs.dedicated,
// config
scalacOptions --= Seq("-Werror", "-Xfatal-warnings"),
mdocIn := file("docs/source"),
@@ -72,14 +65,6 @@ lazy val core: Project =
libraryDependencies ++= ProjectDependencies.Core.dedicated
).dependsOn(testing)
-lazy val config: Project =
- module("config")(
- folder = "./config",
- publishAs = Some(subProjectName("config"))
- ).settings(
- libraryDependencies ++= ProjectDependencies.Config.dedicated
- )
-
lazy val testing: Project =
module("testing")(
folder = "./testing",
@@ -96,13 +81,7 @@ lazy val examples: Project = {
.settings(
noPublishSettings,
Compile / mainClass := Some(s"$appPackage.AppMain"),
- libraryDependencies ++= {
- CrossVersion.partialVersion(scalaVersion.value) match {
- case Some((2, 13)) => ProjectDependencies.Examples.dedicated_2_13
- case Some((3, _)) => ProjectDependencies.Examples.dedicated_3_2
- case _ => Nil
- }
- },
+ libraryDependencies ++= ProjectDependencies.Examples.dedicated,
buildInfoKeys ++= List[BuildInfoKey](
name,
description,
@@ -118,7 +97,7 @@ lazy val examples: Project = {
),
buildInfoPackage := appPackage
)
- .dependsOn(core, config, log4cats, pureconfig)
+ .dependsOn(core, log4cats, pureconfig)
}
// integrations
@@ -145,7 +124,7 @@ lazy val pureconfig: Project =
module("pureconfig")(
folder = s"$integrationsFolder/pureconfig",
publishAs = Some(subProjectName("pureconfig"))
- ).dependsOn(core, config)
+ ).dependsOn(core)
.settings(
libraryDependencies ++= ProjectDependencies.Integrations.Pureconfig.dedicated
)
@@ -214,75 +193,27 @@ lazy val baseSettings: Seq[Def.Setting[_]] = Seq(
versionScheme := Some("early-semver"),
// dependencies
resolvers ++= ProjectResolvers.all,
- libraryDependencies ++= ProjectDependencies.common ++ {
- CrossVersion.partialVersion(scalaVersion.value) match {
- case Some((2, 13)) => ProjectDependencies.Plugins.compilerPluginsFor2_13
- case Some((3, _)) => ProjectDependencies.Plugins.compilerPluginsFor3
- case _ => Nil
- }
- }
+ libraryDependencies ++= Seq(
+ ProjectDependencies.common,
+ ProjectDependencies.Plugins.compilerPlugins
+ ).flatten
)
def scalacSettings(scalaVersion: String): Seq[String] =
Seq(
- // "-Xlog-implicits",
-// "-deprecation", // Emit warning and location for usages of deprecated APIs.
"-encoding",
"utf-8", // Specify character encoding used by source files.
+ "-explain",
+ "-deprecation",
"-feature", // Emit warning and location for usages of features that should be imported explicitly.
"-language:existentials", // Existential types (besides wildcard types) can be written and inferred
-// "-language:experimental.macros", // Allow macro definition (besides implementation and application)
"-language:higherKinds", // Allow higher-kinded types
"-language:implicitConversions", // Allow definition of implicit functions called views
- "-language:dynamics"
- ) ++ {
- CrossVersion.partialVersion(scalaVersion) match {
- case Some((3, _)) =>
- Seq(
- "-Ykind-projector",
- "-explain-types", // Explain type errors in more detail.
- "-Xfatal-warnings" // Fail the compilation if there are any warnings.
- )
- case Some((2, 13)) =>
- Seq(
- "-explaintypes", // Explain type errors in more detail.
- "-unchecked", // Enable additional warnings where generated code depends on assumptions.
- "-Xcheckinit", // Wrap field accessors to throw an exception on uninitialized access.
- "-Xfatal-warnings", // Fail the compilation if there are any warnings.
- "-Xlint:adapted-args", // Warn if an argument list is modified to match the receiver.
- "-Xlint:constant", // Evaluation of a constant arithmetic expression results in an error.
- "-Xlint:delayedinit-select", // Selecting member of DelayedInit.
- "-Xlint:doc-detached", // A Scaladoc comment appears to be detached from its element.
- "-Xlint:inaccessible", // Warn about inaccessible types in method signatures.
- "-Xlint:infer-any", // Warn when a type argument is inferred to be `Any`.
- "-Xlint:missing-interpolator", // A string literal appears to be missing an interpolator id.
- "-Xlint:nullary-unit", // Warn when nullary methods return Unit.
- "-Xlint:option-implicit", // Option.apply used implicit view.
- "-Xlint:package-object-classes", // Class or object defined in package object.
- "-Xlint:poly-implicit-overload", // Parameterized overloaded implicit methods are not visible as view bounds.
- "-Xlint:private-shadow", // A private field (or class parameter) shadows a superclass field.
- "-Xlint:stars-align", // Pattern sequence wildcard must align with sequence component.
- "-Xlint:type-parameter-shadow", // A local type parameter shadows a type already in scope.
- "-Ywarn-dead-code", // Warn when dead code is identified.
- "-Ywarn-extra-implicit", // Warn when more than one implicit parameter section is defined.
- "-Xlint:nullary-unit", // Warn when nullary methods return Unit.
- "-Ywarn-numeric-widen", // Warn when numerics are widened.
- "-Ywarn-value-discard", // Warn when non-Unit expression results are unused.
- "-Xlint:inaccessible", // Warn about inaccessible types in method signatures.
- "-Xlint:infer-any", // Warn when a type argument is inferred to be `Any`.
- "-Ywarn-unused:implicits", // Warn if an implicit parameter is unused.
- "-Ywarn-unused:imports", // Warn if an import selector is not referenced.
- "-Ywarn-unused:locals", // Warn if a local definition is unused.
- "-Ywarn-unused:explicits", // Warn if a explicit value parameter is unused.
- "-Ywarn-unused:patvars", // Warn if a variable bound in a pattern is unused.
- "-Ywarn-unused:privates", // Warn if a private member is unused.
- "-Ywarn-macros:after", // Tells the compiler to make the unused checks after macro expansion
- "-Xsource:3",
- "-P:kind-projector:underscore-placeholders"
- )
- case _ => Nil
- }
- }
+ "-language:dynamics",
+ "-Ykind-projector",
+ "-explain-types", // Explain type errors in more detail.
+ "-Xfatal-warnings" // Fail the compilation if there are any warnings.
+ )
//=============================== ALIASES ===============================
addCommandAlias("check", "scalafmtAll;clean;coverage;test;coverageAggregate")
diff --git a/config/src/main/scala/com/geirolz/app/toolkit/config/BytesUtils.scala b/config/src/main/scala/com/geirolz/app/toolkit/config/BytesUtils.scala
deleted file mode 100644
index 27cb41d..0000000
--- a/config/src/main/scala/com/geirolz/app/toolkit/config/BytesUtils.scala
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.geirolz.app.toolkit.config
-
-import java.nio.ByteBuffer
-import java.util
-
-private[config] object BytesUtils {
-
- def clearByteArray(bytes: Array[Byte]): Null = {
- util.Arrays.fill(bytes, 0.toByte)
- null
- }
-
- def clearByteBuffer(buffer: ByteBuffer): Null = {
- val zeroBytesArray = new Array[Byte](buffer.capacity())
- util.Arrays.fill(zeroBytesArray, 0.toByte)
- buffer.clear()
- buffer.put(zeroBytesArray)
- null
- }
-}
diff --git a/config/src/main/scala/com/geirolz/app/toolkit/config/Secret.scala b/config/src/main/scala/com/geirolz/app/toolkit/config/Secret.scala
deleted file mode 100644
index bfdf4f0..0000000
--- a/config/src/main/scala/com/geirolz/app/toolkit/config/Secret.scala
+++ /dev/null
@@ -1,414 +0,0 @@
-package com.geirolz.app.toolkit.config
-
-import cats.{Eq, MonadError, Show}
-import com.geirolz.app.toolkit.config.BytesUtils.{clearByteArray, clearByteBuffer}
-import com.geirolz.app.toolkit.config.Secret.{DeObfuser, MonadSecretError, Obfuser, ObfuserTuple, PlainValueBuffer, SecretNoLongerValid}
-
-import java.nio.ByteBuffer
-import java.nio.charset.Charset
-import java.security.SecureRandom
-import scala.util.Try
-import scala.util.control.NoStackTrace
-import scala.util.hashing.{Hashing, MurmurHash3}
-
-/** Memory-safe and type-safe secret value of type `T`.
- *
- * `Secret` does the best to avoid leaking information in memory and in the code BUT an attack is possible and I don't give any certainties or
- * guarantees about security using this class, you use it at your own risk. Code is open source, you can check the implementation and take your
- * decision consciously. I'll do my best to improve the security and documentation of this class.
- *
- * Obfuscation
- *
- * The value is obfuscated when creating the `Secret` instance using an implicit `Obfuser`which, by default, transform the value into a xor-ed
- * `ByteBuffer` witch store bytes outside the JVM using direct memory access.
- *
- * The obfuscated value is de-obfuscated using an implicit `DeObfuser` instance every time the method `use` is invoked which returns the original
- * value converting bytes back to `T` re-apply the xor.
- *
- * API and Type safety
- *
- * While obfuscating the value prevents or at least makes it harder to read the value from memory, Secret class API is designed to avoid leaking
- * information in other ways. Preventing developers to improperly use the secret value ( logging, etc...).
- *
- * Example
- * {{{
- * val secretString: Secret[String] = Secret("my_password")
- * val database: F[Database] = secretString.use(password => initDb(password))
- * }}}
- *
- * ** Credits **
- * - Personal experience in companies where I worked
- * - https://westonal.medium.com/protecting-strings-in-jvm-memory-84c365f8f01c
- * - VisualVM
- * - ChatGPT
- */
-sealed trait Secret[T] extends AutoCloseable {
-
- import cats.syntax.all.*
-
- /** Apply `f` with the de-obfuscated value WITHOUT destroying it.
- *
- * If the secret is destroyed it will raise a `NoLongerValidSecret` exception.
- *
- * Once the secret is destroyed it can't be used anymore. If you try to use it using `use`, `useAndDestroy`, `evalUse`, `evalUseAndDestroy` and
- * other methods, it will raise a `NoLongerValidSecret` exception.
- */
- def evalUse[F[_]: MonadSecretError, U](f: T => F[U])(implicit deObfuser: DeObfuser[T]): F[U]
-
- /** Destroy the secret value by filling the obfuscated value with '\0'.
- *
- * This method is idempotent.
- *
- * Once the secret is destroyed it can't be used anymore. If you try to use it using `use`, `useAndDestroy`, `evalUse`, `evalUseAndDestroy` and
- * other methods, it will raise a `NoLongerValidSecret` exception.
- */
- def destroy(): Unit
-
- /** Check if the secret is destroyed
- *
- * @return
- * `true` if the secret is destroyed, `false` otherwise
- */
- def isDestroyed: Boolean
-
- /** Calculate the non-deterministic hash code for this Secret.
- *
- * This hash code is NOT the hash code of the original value. It is the hash code of the obfuscated value.
- *
- * Since the obfuscated value based on a random key, the hash code will be different every time. This function is not deterministic.
- *
- * @return
- * the hash code of this secret. If the secret is destroyed it will return `-1`.
- */
- def hashCode(): Int
-
- // ------------------------------------------------------------------
-
- /** Avoid this method if possible. Unsafely apply `f` with the de-obfuscated value WITHOUT destroying it.
- *
- * If the secret is destroyed it will raise a `NoLongerValidSecret` exception.
- *
- * Throws `SecretNoLongerValid` if the secret is destroyed
- */
- final def unsafeUse[U](f: T => U)(implicit deObfuser: DeObfuser[T]): U =
- use[Either[SecretNoLongerValid, *], U](f).fold(throw _, identity)
-
- /** Apply `f` with the de-obfuscated value WITHOUT destroying it.
- *
- * If the secret is destroyed it will raise a `NoLongerValidSecret` exception.
- *
- * Once the secret is destroyed it can't be used anymore. If you try to use it using `use`, `useAndDestroy`, `evalUse`, `evalUseAndDestroy` and
- * other methods, it will raise a `NoLongerValidSecret` exception.
- */
- final def use[F[_]: MonadSecretError, U](f: T => U)(implicit deObfuser: DeObfuser[T]): F[U] =
- evalUse[F, U](f.andThen(_.pure[F]))
-
- /** Alias for `use` with `Either[Throwable, *]` */
- final def useE[U](f: T => U)(implicit deObfuser: DeObfuser[T]): Either[SecretNoLongerValid, U] =
- use[Either[SecretNoLongerValid, *], U](f)
-
- /** Apply `f` with the de-obfuscated value and then destroy the secret value by invoking `destroy` method.
- *
- * Once the secret is destroyed it can't be used anymore. If you try to use it using `use`, `useAndDestroy`, `evalUse`, `evalUseAndDestroy` and
- * other methods, it will raise a `NoLongerValidSecret` exception.
- */
- final def useAndDestroy[F[_]: MonadSecretError, U](f: T => U)(implicit deObfuser: DeObfuser[T]): F[U] =
- evalUseAndDestroy[F, U](f.andThen(_.pure[F]))
-
- /** Alias for `useAndDestroy` with `Either[Throwable, *]` */
- final def useAndDestroyE[U](f: T => U)(implicit deObfuser: DeObfuser[T]): Either[SecretNoLongerValid, U] =
- useAndDestroy[Either[SecretNoLongerValid, *], U](f)
-
- /** Apply `f` with the de-obfuscated value and then destroy the secret value by invoking `destroy` method.
- *
- * Once the secret is destroyed it can't be used anymore. If you try to use it using `use`, `useAndDestroy`, `evalUse`, `evalUseAndDestroy` and
- * other methods, it will raise a `NoLongerValidSecret` exception.
- */
- final def evalUseAndDestroy[F[_]: MonadSecretError, U](f: T => F[U])(implicit deObfuser: DeObfuser[T]): F[U] =
- evalUse(f).map { u => destroy(); u }
-
- /** Alias for `destroy` */
- final override def close(): Unit = destroy()
-
- /** Safely compare this secret with the provided `Secret`.
- *
- * @return
- * `true` if the secrets are equal, `false` if they are not equal or if one of the secret is destroyed
- */
- final def isEquals(that: Secret[T])(implicit deObfuser: DeObfuser[T]): Boolean =
- evalUse[Try, Boolean](value => that.use[Try, Boolean](_ == value)).getOrElse(false)
-
- /** Always returns `false`, use `isEqual` instead */
- final override def equals(obj: Any): Boolean = false
-
- /** @return
- * always returns a static place holder string "** SECRET **" to avoid leaking information
- */
- final override def toString: String = Secret.placeHolder
-}
-
-object Secret extends Instances {
-
- val placeHolder = "** SECRET **"
- private[config] type PlainValueBuffer = ByteBuffer
- private[config] type ObfuscatedValueBuffer = ByteBuffer
- private[config] type KeyBuffer = ByteBuffer
- private type MonadSecretError[F[_]] = MonadError[F, ? >: SecretNoLongerValid]
-
- case class SecretNoLongerValid() extends RuntimeException("This secret value is no longer valid") with NoStackTrace
- private[Secret] class KeyValueTuple(
- _keyBuffer: KeyBuffer,
- _obfuscatedBuffer: ObfuscatedValueBuffer
- ) {
-
- val roKeyBuffer: KeyBuffer = _keyBuffer.asReadOnlyBuffer()
-
- val roObfuscatedBuffer: ObfuscatedValueBuffer = _obfuscatedBuffer.asReadOnlyBuffer()
-
- lazy val obfuscatedHashCode: Int = {
- val capacity = roObfuscatedBuffer.capacity()
- var bytes: Array[Byte] = new scala.Array[Byte](capacity)
- for (i <- 0 until capacity) {
- bytes(i) = roObfuscatedBuffer.get(i)
- }
- val hashCode: Int = MurmurHash3.bytesHash(bytes)
- bytes = clearByteArray(bytes)
-
- hashCode
- }
-
- def destroy(): Unit = {
- clearByteBuffer(_keyBuffer)
- clearByteBuffer(_obfuscatedBuffer)
- ()
- }
- }
-
- def apply[T: Obfuser](value: T): Secret[T] = {
-
- var bufferTuple: KeyValueTuple = Obfuser[T].apply(value)
-
- new Secret[T] {
-
- override def evalUse[F[_]: MonadSecretError, U](f: T => F[U])(implicit deObfuser: DeObfuser[T]): F[U] =
- if (isDestroyed)
- implicitly[MonadSecretError[F]].raiseError(SecretNoLongerValid())
- else
- f(deObfuser(bufferTuple))
-
- override def destroy(): Unit = {
- bufferTuple.destroy()
- bufferTuple = null
- }
-
- override def isDestroyed: Boolean =
- bufferTuple == null
-
- override def hashCode(): Int =
- if (isDestroyed) -1 else bufferTuple.obfuscatedHashCode
- }
- }
-
- // ---------------- OBFUSER ----------------
- trait Obfuser[P] extends (P => KeyValueTuple)
- object Obfuser {
-
- def apply[P: Obfuser]: Obfuser[P] =
- implicitly[Obfuser[P]]
-
- /** Create a new Obfuser which obfuscate value using a custom formula.
- *
- * @param f
- * the function which obfuscate the value
- */
- def of[P](f: P => KeyValueTuple): Obfuser[P] = f(_)
-
- /** Create a new Obfuser which obfuscate value using a Xor formula.
- *
- * Formula: `plainValue[i] ^ (key[len - i] ^ (len * i))`
- *
- * Example:
- * {{{
- * //Index = 1 2 3 4 5
- * //Plain = [0x01][0x02][0x03][0x04][0x05]
- * //Key = [0x9d][0x10][0xad][0x87][0x2b]
- * //Obfuscated = [0x9c][0x12][0xae][0x83][0x2e]
- * }}}
- */
- def default[P](f: P => PlainValueBuffer): Obfuser[P] = {
-
- def genKeyBuffer(secureRandom: SecureRandom, size: Int): KeyBuffer = {
- val keyBuffer = ByteBuffer.allocateDirect(size)
- var keyArray = new Array[Byte](size)
- secureRandom.nextBytes(keyArray)
- keyBuffer.put(keyArray)
-
- // clear keyArray
- keyArray = clearByteArray(keyArray)
-
- keyBuffer
- }
-
- of { (plain: P) =>
- val secureRandom: SecureRandom = new SecureRandom()
- var plainBuffer: PlainValueBuffer = f(plain)
- val capacity: Int = plainBuffer.capacity()
- val keyBuffer: KeyBuffer = genKeyBuffer(secureRandom, capacity)
- val valueBuffer: ObfuscatedValueBuffer = ByteBuffer.allocateDirect(capacity)
- for (i <- 0 until capacity) {
- valueBuffer.put(
- (
- plainBuffer.get(i) ^ (keyBuffer.get(capacity - 1 - i) ^ (capacity * i).toByte)
- ).toByte
- )
- }
-
- // clear plainBuffer
- plainBuffer = clearByteBuffer(plainBuffer)
-
- new KeyValueTuple(keyBuffer, valueBuffer)
- }
- }
- }
-
- trait DeObfuser[P] extends (KeyValueTuple => P)
- object DeObfuser {
-
- def apply[P: DeObfuser]: DeObfuser[P] =
- implicitly[DeObfuser[P]]
-
- /** Create a new DeObfuser which de-obfuscate value using a custom formula.
- *
- * @param f
- * the function which de-obfuscate the value
- */
- def of[P](f: KeyValueTuple => P): DeObfuser[P] = f(_)
-
- /** Create a new DeObfuser which de-obfuscate value using a Xor formula.
- *
- * Formula: `obfuscated[i] ^ (key[len - i] ^ (len * i))`
- *
- * Example:
- * {{{
- * //Index = 1 2 3 4 5
- * //Obfuscated = [0x9c][0x12][0xae][0x83][0x2e]
- * //Key = [0x9d][0x10][0xad][0x87][0x2b]
- * //Plain = [0x01][0x02][0x03][0x04][0x05]
- * }}}
- */
- def default[P](f: PlainValueBuffer => P): DeObfuser[P] =
- of { bufferTuple =>
- val capacity: Int = bufferTuple.roKeyBuffer.capacity()
- var plainValueBuffer: PlainValueBuffer = ByteBuffer.allocateDirect(capacity)
-
- for (i <- 0 until capacity) {
- plainValueBuffer.put(
- (
- bufferTuple.roObfuscatedBuffer.get(i) ^ (bufferTuple.roKeyBuffer.get(capacity - 1 - i) ^ (capacity * i).toByte)
- ).toByte
- )
- }
-
- val result = f(plainValueBuffer.asReadOnlyBuffer())
-
- // clear plainValueBuffer
- plainValueBuffer = clearByteBuffer(plainValueBuffer)
-
- result
- }
- }
-
- case class ObfuserTuple[P](obfuser: Obfuser[P], deObfuser: DeObfuser[P]) {
- def bimap[U](fO: U => P, fD: P => U): ObfuserTuple[U] =
- ObfuserTuple[U](
- obfuser = Obfuser.of(plain => obfuser(fO(plain))),
- deObfuser = DeObfuser.of(bufferTuple => fD(deObfuser(bufferTuple)))
- )
- }
- object ObfuserTuple {
-
- /** https://westonal.medium.com/protecting-strings-in-jvm-memory-84c365f8f01c
- *
- * We require a buffer that’s outside of the GCs control. This will ensure that multiple copies cannot be left beyond the time we are done with
- * it.
- *
- * For this we can use ByteBuffer.allocateDirect The documentation for https://docs.oracle.com/javase/7/docs/api/java/nio/ByteBuffer.html only
- * says a direct buffer may exist outside of the managed heap but it is at least pinned memory, as they are safe for I/O with non JVM code so the
- * GC won’t be moving this buffer and making copies.
- */
- def withXorDirectByteBuffer[P](capacity: Int)(
- fillBuffer: ByteBuffer => P => ByteBuffer,
- readBuffer: ByteBuffer => P
- ): ObfuserTuple[P] =
- ObfuserTuple(
- obfuser = Obfuser.default((plainValue: P) => fillBuffer(ByteBuffer.allocateDirect(capacity)).apply(plainValue)),
- deObfuser = DeObfuser.default((buffer: PlainValueBuffer) => readBuffer(buffer.rewind().asReadOnlyBuffer()))
- )
-
- def xorStringObfuserTuple(charset: Charset): ObfuserTuple[String] =
- xorBytesArrayObfuserTuple.bimap(_.getBytes(charset), new String(_, charset))
- }
-}
-sealed trait Instances {
-
- implicit val xorBytesArrayObfuserTuple: ObfuserTuple[Array[Byte]] =
- ObfuserTuple[Array[Byte]](
- obfuser = Obfuser.default((plainBytes: Array[Byte]) => ByteBuffer.allocateDirect(plainBytes.length).put(plainBytes)),
- deObfuser = DeObfuser.default((plainBuffer: PlainValueBuffer) => {
- val result = new Array[Byte](plainBuffer.capacity())
- plainBuffer.rewind().get(result)
- result
- })
- )
-
- implicit val xorStdCharsetStringObfuserTuple: ObfuserTuple[String] =
- ObfuserTuple.xorStringObfuserTuple(Charset.defaultCharset())
-
- implicit val xorByteObfuserTuple: ObfuserTuple[Byte] =
- ObfuserTuple.withXorDirectByteBuffer(1)(_.put, _.get)
-
- implicit val xorCharObfuserTuple: ObfuserTuple[Char] =
- ObfuserTuple.withXorDirectByteBuffer(2)(_.putChar, _.getChar)
-
- implicit val xorShortObfuserTuple: ObfuserTuple[Short] =
- ObfuserTuple.withXorDirectByteBuffer(2)(_.putShort, _.getShort)
-
- implicit val xorIntObfuserTuple: ObfuserTuple[Int] =
- ObfuserTuple.withXorDirectByteBuffer(4)(_.putInt, _.getInt)
-
- implicit val xorLongObfuserTuple: ObfuserTuple[Long] =
- ObfuserTuple.withXorDirectByteBuffer(8)(_.putLong, _.getLong)
-
- implicit val xorFloatObfuserTuple: ObfuserTuple[Float] =
- ObfuserTuple.withXorDirectByteBuffer(4)(_.putFloat, _.getFloat)
-
- implicit val xorDoubleObfuserTuple: ObfuserTuple[Double] =
- ObfuserTuple.withXorDirectByteBuffer(8)(_.putDouble, _.getDouble)
-
- implicit val xorBoolObfuserTuple: ObfuserTuple[Boolean] =
- ObfuserTuple.withXorDirectByteBuffer(1)(
- (b: PlainValueBuffer) => (v: Boolean) => b.put(if (v) 1.toByte else 0.toByte),
- _.get == 1.toByte
- )
-
- implicit val bigIntObfuserTuple: ObfuserTuple[BigInt] =
- xorBytesArrayObfuserTuple.bimap(_.toByteArray, BigInt(_))
-
- implicit val bigDecimalObfuserTuple: ObfuserTuple[BigDecimal] =
- xorStdCharsetStringObfuserTuple.bimap(_.toString, str => BigDecimal(str))
-
- implicit def unzipObfuserTupleToObfuser[P: ObfuserTuple]: Obfuser[P] =
- implicitly[ObfuserTuple[P]].obfuser
-
- implicit def unzipObfuserTupleTodeObfuser[P: ObfuserTuple]: DeObfuser[P] =
- implicitly[ObfuserTuple[P]].deObfuser
-
- implicit def hashing[T]: Hashing[Secret[T]] =
- Hashing.fromFunction(_.hashCode())
-
- implicit def eq[T]: Eq[Secret[T]] =
- Eq.fromUniversalEquals
-
- implicit def show[T]: Show[Secret[T]] =
- Show.fromToString
-}
diff --git a/config/src/test/scala/com/geirolz/app/toolkit/config/BytesUtilsSuite.scala b/config/src/test/scala/com/geirolz/app/toolkit/config/BytesUtilsSuite.scala
deleted file mode 100644
index 3c20c82..0000000
--- a/config/src/test/scala/com/geirolz/app/toolkit/config/BytesUtilsSuite.scala
+++ /dev/null
@@ -1,29 +0,0 @@
-package com.geirolz.app.toolkit.config
-
-import java.nio.ByteBuffer
-
-class BytesUtilsSuite extends munit.FunSuite {
-
- test("clearByteArray") {
- val bytes: Array[Byte] = Array[Byte](1, 2, 3, 4, 5)
- BytesUtils.clearByteArray(bytes)
- assertEquals(bytes.toList, List[Byte](0, 0, 0, 0, 0))
- }
-
- test("clearByteBuffer - HeapByteBuffer") {
- val buffer = ByteBuffer.wrap(Array[Byte](1, 2, 3, 4, 5))
- BytesUtils.clearByteBuffer(buffer)
- assertEquals(buffer.array().toList, List[Byte](0, 0, 0, 0, 0))
- }
-
- test("clearByteBuffer - DirectByteBuffer") {
- val buffer = ByteBuffer.allocateDirect(5)
- buffer.put(Array[Byte](1, 2, 3, 4, 5))
- BytesUtils.clearByteBuffer(buffer)
-
- val array = new Array[Byte](buffer.capacity())
- buffer.rewind().get(array)
-
- assertEquals(array.toList, List[Byte](0, 0, 0, 0, 0))
- }
-}
diff --git a/config/src/test/scala/com/geirolz/app/toolkit/config/SecretSuite.scala b/config/src/test/scala/com/geirolz/app/toolkit/config/SecretSuite.scala
deleted file mode 100644
index de6cb20..0000000
--- a/config/src/test/scala/com/geirolz/app/toolkit/config/SecretSuite.scala
+++ /dev/null
@@ -1,142 +0,0 @@
-package com.geirolz.app.toolkit.config
-
-import com.geirolz.app.toolkit.config.Secret.{ObfuserTuple, SecretNoLongerValid}
-import org.scalacheck.Arbitrary
-import org.scalacheck.Prop.forAll
-
-import scala.reflect.ClassTag
-import scala.util.{Failure, Try}
-
-class SecretSuite extends munit.ScalaCheckSuite {
-
- testObfuserTupleFor[String]
- testObfuserTupleFor[Int]
- testObfuserTupleFor[Long]
- testObfuserTupleFor[Short]
- testObfuserTupleFor[Char]
- testObfuserTupleFor[Byte]
- testObfuserTupleFor[Float]
- testObfuserTupleFor[Double]
- testObfuserTupleFor[Boolean]
- testObfuserTupleFor[BigInt]
- testObfuserTupleFor[BigDecimal]
-
- test("Simple Secret String") {
- Secret("TEST").useAndDestroyE(_ => ())
- }
-
- test("Simple Secret with long String") {
- Secret(
- """|C#iur0#UsxTWzUZ5QPn%KGo$922SMvc5zYLqrcdE6SU6ZpFQrk3&W
- |1c48obb&Rngv9twgMHTuXG@hRb@FZg@u!uPoG%dxTab0QtTab0Qta
- |c5zYU6ZpRngv9twgMHTuXGFdxTab0QtTab0QtaKGo$922SMvc5zYL
- |KGo$922SMvc5zYLqrcdEKGo$922SMvc5zYLqrcdE6SU6ZpFQrk36S
- |U6ZpFQrk31hRbc48obb1c48obb&Rngv9twgMHTuXG@hRb@FZg@u!u
- |PoG%dxTab0QtTab0QtaKGo$922SMvc5zYLqrcdEKGo$922SMvc5zY
- |LqrcdE6SdxTab0QtTab0QtaKGo$922SMvc5zYLqrcdEKGo$922SMv
- |c5zYU6ZpRngv9twgMHTuXGFdxTab0QtTab0QtaKGo$922SMvc5zYL
- |1c48obb&Rngv9twgMHTuXG@hRb@FZg@u!uPoG%dxTab0QtTab0Qta
- |c5zYU6ZpRngv9twgMHTuXGFdxTab0QtTab0QtaKGo$922SMvc5zYL
- |KGo$922SMvc5zYLqrcdEKGo$922SMvc5zYLqrcdE6SU6ZpFQrk36S
- |U6ZpFQrk31hRbc48obb1c48obb&Rngv9twgMHTuXG@hRb@FZg@u!u
- |PoG%dxTab0QtTab0QtaKGo$922SMvc5zYLqrcdEKGo$922SMvc5zY
- |LqrcdE6SdxTab0QtTab0QtaKGo$922SMvc5zYLqrcdEKGo$922SMv
- |c5zYU6ZpRngv9twgMHTuXGFdxTab0QtTab0QtaKGo$922SMvc5zYL
- |qrcdEKGo$922SMvc5zYU6ZpFQrk31hRbc48obb1c48obbQrqgk36S
- |PoG%dxTab0QtTab0QtaKGo$922SMvc5zYLqrcdEKGo$922SMvc5zY
- |LqrcdE6SdxTab0QtTab0QtaKGo$922SMvc5zYLqrcdEKGo$922SMv
- |c5zYU6ZpRngv9twgMHTuXGFdxTab0QtTab0QtaKGo$922SMvc5zYL
- |1c48obb&Rngv9twgMHTuXG@hRb@FZg@u!uPoG%dxTab0QtTab0Qta
- |c5zYU6ZpRngv9twgMHTuXGFdxTab0QtTab0QtaKGo$922SMvc5zYL
- |KGo$922SMvc5zYLqrcdEKGo$922SMvc5zYLqrcdE6SU6ZpFQrk36S
- |U6ZpFQrk31hRbc48obb1c48obb&Rngv9twgMHTuXG@hRb@FZg@u!u
- |PoG%dxTab0QtTab0QtaKGo$922SMvc5zYLqrcdEKGo$922SMvc5zY
- |LqrcdE6SdxTab0QtTab0QtaKGo$922SMvc5zYLqrcdEKGo$922SMv
- |c5zYU6ZpRngv9twgMHTuXGFdxTab0QtTab0QtaKGo$922SMvc5zYL
- |qrcdEKGo$922SMvc5zYU6ZpFQrk31hRbc48obb1c48obbQrqgk36S
- |qrcdEKGo$922SMvc5zYU6ZpFQrk31hRbc48obb1c48obbQrqgk36S
- |""".stripMargin
- ).useAndDestroyE(_ => ())
- }
-
- private def testObfuserTupleFor[T: Arbitrary: ObfuserTuple](implicit c: ClassTag[T]): Unit = {
-
- val typeName = c.runtimeClass.getSimpleName.capitalize
-
- property(s"Secret[$typeName] succesfully obfuscate") {
- forAll { (value: T) =>
- Secret(value)
- assert(cond = true)
- }
- }
-
- property(s"Secret[$typeName] equals always return false") {
- forAll { (value: T) =>
- assertNotEquals(Secret(value), Secret(value))
- }
- }
-
- property(s"Secret[$typeName] isEquals works properly") {
- forAll { (value: T) =>
- val s1 = Secret(value)
- val s2 = Secret(value)
-
- assert(s1.isEquals(s2))
- s1.destroy()
- assert(!s1.isEquals(s2))
- assert(!s2.isEquals(s1))
- s2.destroy()
- assert(!s1.isEquals(s2))
- }
- }
-
- property(s"Secret[$typeName] hashCode is different from the value one") {
- forAll { (value: T) =>
- assert(Secret(value).hashCode() != value.hashCode())
- }
- }
-
- // use
- property(s"Secret[$typeName] obfuscate and de-obfuscate properly - use") {
- forAll { (value: T) =>
- assert(
- Secret(value)
- .use[Try, Unit](result => {
- assertEquals(
- obtained = result,
- expected = value
- )
- })
- .isSuccess
- )
- }
- }
-
- // useAndDestroy
- property(s"Secret[$typeName] obfuscate and de-obfuscate properly - useAndDestroy") {
- forAll { (value: T) =>
- val secret: Secret[T] = Secret(value)
-
- assert(
- secret
- .useAndDestroy[Try, Unit] { result =>
- assertEquals(
- obtained = result,
- expected = value
- )
- }
- .isSuccess
- )
-
- assertEquals(
- obtained = secret.useAndDestroy[Try, Int](_.hashCode()),
- expected = Failure(SecretNoLongerValid())
- )
- assertEquals(
- obtained = secret.isDestroyed,
- expected = true
- )
- }
- }
- }
-}
diff --git a/config/src/test/scala/com/geirolz/app/toolkit/config/testing/Gens.scala b/config/src/test/scala/com/geirolz/app/toolkit/config/testing/Gens.scala
deleted file mode 100644
index acfc1c8..0000000
--- a/config/src/test/scala/com/geirolz/app/toolkit/config/testing/Gens.scala
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.geirolz.app.toolkit.config.testing
-
-import org.scalacheck.Gen
-
-object Gens {
-
- def strGen(size: Int): Gen[String] = Gen.listOfN(size, Gen.alphaChar).map(_.mkString)
-}
diff --git a/config/src/test/scala/com/geirolz/app/toolkit/config/testing/Timed.scala b/config/src/test/scala/com/geirolz/app/toolkit/config/testing/Timed.scala
deleted file mode 100644
index 15248c6..0000000
--- a/config/src/test/scala/com/geirolz/app/toolkit/config/testing/Timed.scala
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.geirolz.app.toolkit.config.testing
-
-import java.util.concurrent.TimeUnit
-import scala.concurrent.duration.FiniteDuration
-
-object Timed {
- def apply[T](f: => T): (FiniteDuration, T) = {
- val start = System.nanoTime()
- val result = f
- val end = System.nanoTime()
- val duration = FiniteDuration(end - start, TimeUnit.NANOSECONDS)
- (duration, result)
- }
-}
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/App.scala b/core/src/main/scala/com/geirolz/app/toolkit/App.scala
index aab8726..502c07c 100644
--- a/core/src/main/scala/com/geirolz/app/toolkit/App.scala
+++ b/core/src/main/scala/com/geirolz/app/toolkit/App.scala
@@ -1,94 +1,62 @@
package com.geirolz.app.toolkit
-import cats.data.NonEmptyList
import cats.effect.*
-import cats.{Endo, Foldable, Parallel, Semigroup, Show}
-import com.geirolz.app.toolkit.FailureHandler.OnFailureBehaviour
-import com.geirolz.app.toolkit.error.MultiException
-import com.geirolz.app.toolkit.logger.{LoggerAdapter, NoopLogger}
-import com.geirolz.app.toolkit.novalues.{NoConfig, NoDependencies, NoResources}
+import cats.syntax.all.given
+import cats.{Endo, Parallel, Show}
+import com.geirolz.app.toolkit.failure.FailureHandler
+import com.geirolz.app.toolkit.failure.FailureHandler.OnFailureBehaviour
+import com.geirolz.app.toolkit.logger.LoggerAdapter
+import com.geirolz.app.toolkit.novalues.NoFailure.NotNoFailure
+import com.geirolz.app.toolkit.novalues.NoFailure
+
+import scala.reflect.ClassTag
class App[
F[+_]: Async: Parallel,
FAILURE,
- APP_INFO <: SimpleAppInfo[?],
+ INFO <: SimpleAppInfo[?],
LOGGER_T[_[_]]: LoggerAdapter,
CONFIG: Show,
RESOURCES,
DEPENDENCIES
-] private[App] (
- val appInfo: APP_INFO,
- val appMessages: AppMessages,
+] private[toolkit] (
+ val info: INFO,
+ val messages: AppMessages,
val loggerBuilder: F[LOGGER_T[F]],
val configLoader: Resource[F, CONFIG],
val resourcesLoader: Resource[F, RESOURCES],
- val beforeProvidingF: App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit],
- val onFinalizeF: App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit],
- val failureHandlerLoader: App.Resources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => FailureHandler[F, FAILURE],
- val dependenciesLoader: App.Resources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, FAILURE \/ DEPENDENCIES],
- val provideBuilder: App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[FAILURE \/ List[F[FAILURE \/ Any]]]
-) {
- type AppInfo = APP_INFO
- type Logger = LOGGER_T[F]
- type Config = CONFIG
-
- import cats.syntax.all.*
-
- type Self = App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES]
- type AppResources = App.Resources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES]
-
- class Resourced[T] private (val appRes: AppResources, val value: T) {
- val info: APP_INFO = appRes.info
- val config: CONFIG = appRes.config
- val logger: LOGGER_T[F] = appRes.logger
- val resources: RESOURCES = appRes.resources
-
- def map[U](f: T => U): Resourced[U] = Resourced(appRes, f(value))
- def tupled: (AppResources, T) = Resourced.unapply(this)
- def useTupled[U](f: (AppResources, T) => U): U = f.tupled(tupled)
- def use[U](f: AppResources => T => U): U = f(appRes)(value)
-
- def tupledAll: (APP_INFO, CONFIG, LOGGER_T[F], RESOURCES, T) = (info, config, logger, resources, value)
- def useTupledAll[U](f: (APP_INFO, CONFIG, LOGGER_T[F], RESOURCES, T) => U): U = f.tupled(tupledAll)
- }
- object Resourced {
- def apply[T](appRes: AppResources, value: T): Resourced[T] = new Resourced[T](appRes, value)
- def unapply[T](r: Resourced[T]): (AppResources, T) = (r.appRes, r.value)
- }
-
- def withMessages(messages: AppMessages): Self =
- copyWith(appMessages = messages)
-
- def onFinalize(
- f: App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit]
- ): Self =
- copyWith(onFinalizeF = f)
-
- def onFinalizeSeq(
- f: App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit],
- fN: App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit]*
- ): Self =
- onFinalizeSeq(deps => (f +: fN).map(_(deps)))
-
- def onFinalizeSeq[G[_]: Foldable](
- f: App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => G[F[Unit]]
- ): Self =
- copyWith(onFinalizeF = f(_).sequence_)
+ val beforeProvidingTask: AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit],
+ val onFinalizeTask: AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit],
+ val failureHandlerLoader: AppContext.NoDeps[INFO, LOGGER_T[F], CONFIG, RESOURCES] ?=> FailureHandler[F, FAILURE],
+ val depsLoader: AppContext.NoDeps[INFO, LOGGER_T[F], CONFIG, RESOURCES] ?=> Resource[F, FAILURE \/ DEPENDENCIES],
+ val servicesBuilder: AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[FAILURE \/ List[F[FAILURE \/ Unit]]]
+):
+ type AppInfo = INFO
+ type Logger = LOGGER_T[F]
+ type Config = CONFIG
+ type ContextNoDeps = AppContext.NoDeps[INFO, LOGGER_T[F], CONFIG, RESOURCES]
+
+ inline def onFinalize(
+ f: AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] ?=> F[Unit]
+ ): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
+ copyWith(onFinalizeTask = deps => this.onFinalizeTask(deps) >> f(using deps))
+
+ // compile and run
+ inline def compile[R[_]](
+ appArgs: List[String] = Nil
+ )(using c: AppCompiler[F], i: AppLogicInterpreter[F, R, FAILURE]): Resource[F, F[R[Unit]]] =
+ i.interpret(c.compile(appArgs, this))
+
+ inline def run[R[_]](appArgs: List[String] = Nil)(using c: AppCompiler[F], i: AppLogicInterpreter[F, R, FAILURE]): F[ExitCode] =
+ runRaw[R](appArgs)
+ .map(i.isSuccess(_))
+ .ifF(
+ ifTrue = ExitCode.Success,
+ ifFalse = ExitCode.Error
+ )
- private[toolkit] def _updateFailureHandlerLoader(
- fh: App.Resources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => Endo[FailureHandler[F, FAILURE]]
- ): App[
- F,
- FAILURE,
- APP_INFO,
- LOGGER_T,
- CONFIG,
- RESOURCES,
- DEPENDENCIES
- ] =
- copyWith(
- failureHandlerLoader = appRes => fh(appRes)(failureHandlerLoader(appRes))
- )
+ inline def runRaw[R[_]](appArgs: List[String] = Nil)(using c: AppCompiler[F], i: AppLogicInterpreter[F, R, FAILURE]): F[R[Unit]] =
+ compile(appArgs).useEval
private[toolkit] def copyWith[
G[+_]: Async: Parallel,
@@ -99,498 +67,88 @@ class App[
RES2,
DEPS2
](
- appInfo: APP_INFO2 = this.appInfo,
- appMessages: AppMessages = this.appMessages,
- loggerBuilder: G[LOGGER_T2[G]] = this.loggerBuilder,
- configLoader: Resource[G, CONFIG2] = this.configLoader,
- resourcesLoader: Resource[G, RES2] = this.resourcesLoader,
- beforeProvidingF: App.Dependencies[APP_INFO2, LOGGER_T2[G], CONFIG2, DEPS2, RES2] => G[Unit] = this.beforeProvidingF,
- onFinalizeF: App.Dependencies[APP_INFO2, LOGGER_T2[G], CONFIG2, DEPS2, RES2] => G[Unit] = this.onFinalizeF,
- failureHandlerLoader: App.Resources[APP_INFO2, LOGGER_T2[G], CONFIG2, RES2] => FailureHandler[
- G,
- FAILURE2
- ] = this.failureHandlerLoader,
- dependenciesLoader: App.Resources[APP_INFO2, LOGGER_T2[G], CONFIG2, RES2] => Resource[
- G,
- FAILURE2 \/ DEPS2
- ] = this.dependenciesLoader,
- provideBuilder: App.Dependencies[APP_INFO2, LOGGER_T2[G], CONFIG2, DEPS2, RES2] => G[
- FAILURE2 \/ List[G[FAILURE2 \/ Any]]
- ] = this.provideBuilder
+ appInfo: APP_INFO2 = this.info,
+ appMessages: AppMessages = this.messages,
+ loggerBuilder: G[LOGGER_T2[G]] = this.loggerBuilder,
+ configLoader: Resource[G, CONFIG2] = this.configLoader,
+ resourcesLoader: Resource[G, RES2] = this.resourcesLoader,
+ beforeProvidingTask: AppContext[APP_INFO2, LOGGER_T2[G], CONFIG2, DEPS2, RES2] => G[Unit] = this.beforeProvidingTask,
+ onFinalizeTask: AppContext[APP_INFO2, LOGGER_T2[G], CONFIG2, DEPS2, RES2] => G[Unit] = this.onFinalizeTask,
+ failureHandlerLoader: AppContext.NoDeps[APP_INFO2, LOGGER_T2[G], CONFIG2, RES2] ?=> FailureHandler[G, FAILURE2] = this.failureHandlerLoader,
+ dependenciesLoader: AppContext.NoDeps[APP_INFO2, LOGGER_T2[G], CONFIG2, RES2] ?=> Resource[G, FAILURE2 \/ DEPS2] = this.depsLoader,
+ provideBuilder: AppContext[APP_INFO2, LOGGER_T2[G], CONFIG2, DEPS2, RES2] => G[FAILURE2 \/ List[G[FAILURE2 \/ Unit]]] = this.servicesBuilder
): App[G, FAILURE2, APP_INFO2, LOGGER_T2, CONFIG2, RES2, DEPS2] =
new App[G, FAILURE2, APP_INFO2, LOGGER_T2, CONFIG2, RES2, DEPS2](
- appInfo = appInfo,
- appMessages = appMessages,
+ info = appInfo,
+ messages = appMessages,
loggerBuilder = loggerBuilder,
configLoader = configLoader,
resourcesLoader = resourcesLoader,
- beforeProvidingF = beforeProvidingF,
- onFinalizeF = onFinalizeF,
+ beforeProvidingTask = beforeProvidingTask,
+ onFinalizeTask = onFinalizeTask,
failureHandlerLoader = failureHandlerLoader,
- dependenciesLoader = dependenciesLoader,
- provideBuilder = provideBuilder
+ depsLoader = dependenciesLoader,
+ servicesBuilder = provideBuilder
)
-}
-object App extends AppSyntax {
-
- import cats.syntax.all.*
-
- final case class Dependencies[APP_INFO <: SimpleAppInfo[?], LOGGER, CONFIG, DEPENDENCIES, RESOURCES](
- private val _resources: App.Resources[APP_INFO, LOGGER, CONFIG, RESOURCES],
- private val _dependencies: DEPENDENCIES
- ) {
- // proxies
- val info: APP_INFO = _resources.info
- val args: AppArgs = _resources.args
- val logger: LOGGER = _resources.logger
- val config: CONFIG = _resources.config
- val resources: RESOURCES = _resources.resources
- val dependencies: DEPENDENCIES = _dependencies
-
- override def toString: String =
- s"""App.Dependencies(
- | info = $info,
- | args = $args,
- | logger = $logger,
- | config = $config,
- | resources = $resources,
- | dependencies = $dependencies
- |)""".stripMargin
- }
- object Dependencies {
-
- private[toolkit] def apply[APP_INFO <: SimpleAppInfo[?], LOGGER, CONFIG, DEPENDENCIES, RESOURCES](
- resources: App.Resources[APP_INFO, LOGGER, CONFIG, RESOURCES],
- dependencies: DEPENDENCIES
- ): Dependencies[APP_INFO, LOGGER, CONFIG, DEPENDENCIES, RESOURCES] =
- new Dependencies[APP_INFO, LOGGER, CONFIG, DEPENDENCIES, RESOURCES](
- _resources = resources,
- _dependencies = dependencies
- )
-
- def unapply[APP_INFO <: SimpleAppInfo[?], LOGGER, CONFIG, DEPENDENCIES, RESOURCES](
- deps: Dependencies[APP_INFO, LOGGER, CONFIG, DEPENDENCIES, RESOURCES]
- ): Option[(APP_INFO, AppArgs, LOGGER, CONFIG, RESOURCES, DEPENDENCIES)] =
- Some(
- (
- deps.info,
- deps.args,
- deps.logger,
- deps.config,
- deps.resources,
- deps.dependencies
- )
- )
- }
-
- final case class Resources[APP_INFO <: SimpleAppInfo[?], LOGGER, CONFIG, RESOURCES](
- info: APP_INFO,
- args: AppArgs,
- logger: LOGGER,
- config: CONFIG,
- resources: RESOURCES
- ) {
- type AppInfo = APP_INFO
- type Logger = LOGGER
- type Config = CONFIG
- type Resources = RESOURCES
-
- override def toString: String =
- s"""App.Dependencies(
- | info = $info,
- | args = $args,
- | logger = $logger,
- | config = $config,
- | resources = $resources
- |)""".stripMargin
- }
- object Resources {
-
- private[toolkit] def apply[APP_INFO <: SimpleAppInfo[?], LOGGER, CONFIG, RESOURCES](
- info: APP_INFO,
- args: AppArgs,
- logger: LOGGER,
- config: CONFIG,
- resources: RESOURCES
- ): Resources[APP_INFO, LOGGER, CONFIG, RESOURCES] =
- new Resources[APP_INFO, LOGGER, CONFIG, RESOURCES](
- info = info,
- args = args,
- logger = logger,
- config = config,
- resources = resources
- )
-
- def unapply[APP_INFO <: SimpleAppInfo[?], LOGGER, CONFIG, RESOURCES](
- res: Resources[APP_INFO, LOGGER, CONFIG, RESOURCES]
- ): Option[(APP_INFO, AppArgs, LOGGER, CONFIG, RESOURCES)] =
- Some(
- (
- res.info,
- res.args,
- res.logger,
- res.config,
- res.resources
- )
- )
- }
-
- def apply[F[+_]: Async: Parallel](implicit dummyImplicit: DummyImplicit): AppBuilderRuntimeSelected[F, Throwable] =
- App[F, Throwable]
-
- def apply[F[+_]: Async: Parallel, FAILURE]: AppBuilderRuntimeSelected[F, FAILURE] =
- new AppBuilderRuntimeSelected[F, FAILURE]
-
- final class AppBuilderRuntimeSelected[F[+_]: Async: Parallel, FAILURE] private[App] () {
- def withInfo[APP_INFO <: SimpleAppInfo[?]](
- appInfo: APP_INFO
- ): AppBuilderSelectResAndDeps[F, FAILURE, APP_INFO, NoopLogger, NoConfig, NoResources] =
- new AppBuilderSelectResAndDeps[F, FAILURE, APP_INFO, NoopLogger, NoConfig, NoResources](
- appInfo = appInfo,
- loggerBuilder = NoopLogger[F].pure[F],
- configLoader = Resource.pure(NoConfig.value),
- resourcesLoader = Resource.pure(NoResources.value)
- )
- }
-
- final class AppBuilderSelectResAndDeps[F[+_]: Async: Parallel, FAILURE, APP_INFO <: SimpleAppInfo[
- ?
- ], LOGGER_T[
- _[_]
- ]: LoggerAdapter, CONFIG: Show, RESOURCES] private[App] (
- appInfo: APP_INFO,
- loggerBuilder: F[LOGGER_T[F]],
- configLoader: Resource[F, CONFIG],
- resourcesLoader: Resource[F, RESOURCES]
- ) {
-
- // ------- LOGGER -------
- def withNoopLogger: AppBuilderSelectResAndDeps[F, FAILURE, APP_INFO, NoopLogger, CONFIG, RESOURCES] =
- withLogger(logger = NoopLogger[F])
-
- def withLogger[LOGGER_T2[_[_]]: LoggerAdapter](
- logger: LOGGER_T2[F]
- ): AppBuilderSelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T2, CONFIG, RESOURCES] =
- withLogger[LOGGER_T2](loggerF = (_: APP_INFO) => logger)
-
- def withLogger[LOGGER_T2[_[_]]: LoggerAdapter](
- loggerF: APP_INFO => LOGGER_T2[F]
- ): AppBuilderSelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T2, CONFIG, RESOURCES] =
- withLoggerBuilder(loggerBuilder = appInfo => loggerF(appInfo).pure[F])
-
- def withLoggerBuilder[LOGGER_T2[_[_]]: LoggerAdapter](
- loggerBuilder: APP_INFO => F[LOGGER_T2[F]]
- ): AppBuilderSelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T2, CONFIG, RESOURCES] =
- copyWith(loggerBuilder = loggerBuilder(appInfo))
-
- // ------- CONFIG -------
- def withoutConfig: AppBuilderSelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, NoConfig, RESOURCES] =
- withConfig[NoConfig](NoConfig.value)
-
- def withConfig[CONFIG2: Show](
- config: CONFIG2
- ): AppBuilderSelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG2, RESOURCES] =
- withConfigLoader(config.pure[F])
-
- def withConfigLoader[CONFIG2: Show](
- configLoader: APP_INFO => F[CONFIG2]
- )(implicit dummyImplicit: DummyImplicit): AppBuilderSelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG2, RESOURCES] =
- withConfigLoader(i => Resource.eval(configLoader(i)))
-
- def withConfigLoader[CONFIG2: Show](
- configLoader: F[CONFIG2]
- ): AppBuilderSelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG2, RESOURCES] =
- withConfigLoader(Resource.eval(configLoader))
-
- def withConfigLoader[CONFIG2: Show](
- configLoader: Resource[F, CONFIG2]
- ): AppBuilderSelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG2, RESOURCES] =
- withConfigLoader(_ => configLoader)
-
- def withConfigLoader[CONFIG2: Show](
- configLoader: APP_INFO => Resource[F, CONFIG2]
- ): AppBuilderSelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG2, RESOURCES] =
- copyWith(configLoader = configLoader(this.appInfo))
- // ------- RESOURCES -------
- def withoutResources: AppBuilderSelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, NoResources] =
- withResources[NoResources](NoResources.value)
+object App extends AppFailureSyntax:
- def withResources[RESOURCES2](
- resources: RESOURCES2
- ): AppBuilderSelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES2] =
- withResourcesLoader(resources.pure[F])
+ type Simple[F[+_], INFO <: SimpleAppInfo[?], LOGGER_T[_[_]], CONFIG, RESOURCES, DEPENDENCIES] =
+ App[F, NoFailure, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES]
- def withResourcesLoader[RESOURCES2](
- resourcesLoader: F[RESOURCES2]
- ): AppBuilderSelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES2] =
- withResourcesLoader(Resource.eval(resourcesLoader))
+ inline def apply[F[+_]: Async: Parallel]: AppBuilder.Simple[F] =
+ AppBuilder.simple[F]
- def withResourcesLoader[RESOURCES2](
- resourcesLoader: Resource[F, RESOURCES2]
- ): AppBuilderSelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES2] =
- copyWith(resourcesLoader = resourcesLoader)
+ inline def apply[F[+_]: Async: Parallel, FAILURE: ClassTag]: AppBuilder[F, FAILURE] =
+ AppBuilder.withFailure[F, FAILURE]
- // ------- DEPENDENCIES -------
- def withoutDependencies: AppBuilderSelectProvide[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, NoDependencies] =
- dependsOn[NoDependencies, FAILURE](_ => Resource.pure(NoDependencies.value.asRight[FAILURE]))
+sealed transparent trait AppFailureSyntax:
- def dependsOn[DEPENDENCIES](
- f: App.Resources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, DEPENDENCIES]
- ): AppBuilderSelectProvide[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
- dependsOn[DEPENDENCIES, FAILURE](f.andThen(_.map(_.asRight[FAILURE])))
-
- def dependsOn[DEPENDENCIES, FAILURE2 <: FAILURE](
- f: App.Resources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, FAILURE2 \/ DEPENDENCIES]
- )(implicit dummyImplicit: DummyImplicit): AppBuilderSelectProvide[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
- AppBuilderSelectProvide(
- appInfo = appInfo,
- loggerBuilder = loggerBuilder,
- configLoader = configLoader,
- resourcesLoader = resourcesLoader,
- dependenciesLoader = f,
- beforeProvidingF = _ => ().pure[F]
- )
-
- private def copyWith[G[+_]: Async: Parallel, ERROR2, APP_INFO2 <: SimpleAppInfo[?], LOGGER_T2[
- _[_]
- ]: LoggerAdapter, CONFIG2: Show, RESOURCES2](
- appInfo: APP_INFO2 = this.appInfo,
- loggerBuilder: G[LOGGER_T2[G]] = this.loggerBuilder,
- configLoader: Resource[G, CONFIG2] = this.configLoader,
- resourcesLoader: Resource[G, RESOURCES2] = this.resourcesLoader
- ) = new AppBuilderSelectResAndDeps[G, ERROR2, APP_INFO2, LOGGER_T2, CONFIG2, RESOURCES2](
- appInfo = appInfo,
- loggerBuilder = loggerBuilder,
- configLoader = configLoader,
- resourcesLoader = resourcesLoader
- )
- }
-
- final case class AppBuilderSelectProvide[
+ extension [
F[+_]: Async: Parallel,
- FAILURE,
- APP_INFO <: SimpleAppInfo[?],
+ FAILURE: NotNoFailure,
+ INFO <: SimpleAppInfo[?],
LOGGER_T[_[_]]: LoggerAdapter,
- CONFIG: Show,
- RESOURCES,
- DEPENDENCIES
- ](
- private val appInfo: APP_INFO,
- private val loggerBuilder: F[LOGGER_T[F]],
- private val configLoader: Resource[F, CONFIG],
- private val resourcesLoader: Resource[F, RESOURCES],
- private val dependenciesLoader: App.Resources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[
- F,
- FAILURE \/ DEPENDENCIES
- ],
- private val beforeProvidingF: App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit]
- ) {
-
- // ------- BEFORE PROVIDING -------
- def beforeProviding(
- f: App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit]
- ): AppBuilderSelectProvide[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
- copy(beforeProvidingF = f)
-
- def beforeProvidingSeq(
- f: App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit],
- fN: App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit]*
- ): AppBuilderSelectProvide[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
- beforeProvidingSeq(deps => (f +: fN).map(_(deps)))
-
- def beforeProvidingSeq[G[_]: Foldable](
- f: App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => G[F[Unit]]
- ): AppBuilderSelectProvide[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
- copy(beforeProvidingF = f(_).sequence_)
-
- // ------- PROVIDE -------
- def provideOne(
- f: App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Any]
- )(implicit
- env: FAILURE =:= Throwable
- ): App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
- provideOne[FAILURE](f.andThen(_.map(_.asRight[FAILURE])))
-
- def provideOne[FAILURE2 <: FAILURE](
- f: App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[FAILURE2 \/ Any]
- ): App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
- provide[FAILURE2](f.andThen(List(_)))
-
- def provideOneF[FAILURE2 <: FAILURE](
- f: App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[FAILURE2 \/ F[Any]]
- ): App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
- provideAttemptF[FAILURE2](f.andThen((fa: F[FAILURE2 \/ F[Any]]) => fa.map(_.map(v => List(v.map(_.asRight[FAILURE2]))))))
-
- // provide
- def provide(
- f: App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => List[F[Any]]
- ): App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
- provide[FAILURE](f.andThen(_.map(_.map(_.asRight[FAILURE]))))
-
- def provide[FAILURE2 <: FAILURE](
- f: App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => List[F[FAILURE2 \/ Any]]
- )(implicit dummyImplicit: DummyImplicit): App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
- provideF[FAILURE2](f.andThen(_.pure[F]))
-
- // provideF
- def provideF(
- f: App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[List[F[Any]]]
- ): App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
- provideF[FAILURE](f.andThen(_.map(_.map(_.map(_.asRight[FAILURE])))))
-
- def provideF[FAILURE2 <: FAILURE](
- f: App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[List[F[FAILURE2 \/ Any]]]
- )(implicit dummyImplicit: DummyImplicit): App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
- provideAttemptF(f.andThen(_.map(Right(_))))
-
- def provideAttemptF[FAILURE2 <: FAILURE](
- f: App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[FAILURE2 \/ List[F[FAILURE2 \/ Any]]]
- ): App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
- new App(
- appInfo = appInfo,
- appMessages = AppMessages.default(appInfo),
- failureHandlerLoader = _ => FailureHandler.cancelAll,
- loggerBuilder = loggerBuilder,
- resourcesLoader = resourcesLoader,
- beforeProvidingF = beforeProvidingF,
- onFinalizeF = _ => ().pure[F],
- configLoader = configLoader,
- dependenciesLoader = dependenciesLoader,
- provideBuilder = f
- )
- }
- object AppBuilderSelectProvide {
- private[App] def apply[
- F[+_]: Async: Parallel,
- FAILURE,
- APP_INFO <: SimpleAppInfo[?],
- LOGGER_T[_[_]]: LoggerAdapter,
- CONFIG: Show,
- RESOURCES,
- DEPENDENCIES
- ](
- appInfo: APP_INFO,
- loggerBuilder: F[LOGGER_T[F]],
- configLoader: Resource[F, CONFIG],
- resourcesLoader: Resource[F, RESOURCES],
- dependenciesLoader: App.Resources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[
- F,
- FAILURE \/ DEPENDENCIES
- ],
- beforeProvidingF: App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit]
- ): AppBuilderSelectProvide[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
- new AppBuilderSelectProvide[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES](
- appInfo = appInfo,
- loggerBuilder = loggerBuilder,
- configLoader = configLoader,
- resourcesLoader = resourcesLoader,
- dependenciesLoader = dependenciesLoader,
- beforeProvidingF = beforeProvidingF
- )
- }
-}
-sealed trait AppSyntax {
-
- import cats.syntax.all.*
-
- implicit class AppOps[F[
- +_
- ]: Async: Parallel, FAILURE, APP_INFO <: SimpleAppInfo[
- ?
- ], LOGGER_T[
- _[_]
- ]: LoggerAdapter, CONFIG: Show, RESOURCES, DEPENDENCIES](
- val app: App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES]
- )(implicit env: FAILURE =:!= Throwable) {
+ CONFIG: ClassTag: Show,
+ RESOURCES: ClassTag,
+ DEPENDENCIES: ClassTag
+ ](app: App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES])
// failures
- def mapFailure[FAILURE2](
- fhLoader: App.Resources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => FailureHandler[
- F,
- FAILURE2
- ]
+ inline def mapFailure[FAILURE2](
+ fhLoader: AppContext.NoDeps[INFO, LOGGER_T[F], CONFIG, RESOURCES] ?=> FailureHandler[F, FAILURE2]
)(
f: FAILURE => FAILURE2
- ): App[F, FAILURE2, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
- app.copyWith[F, FAILURE2, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES](
+ )(using NotNoFailure[FAILURE2]): App[F, FAILURE2, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
+ app.copyWith[F, FAILURE2, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES](
failureHandlerLoader = fhLoader,
- dependenciesLoader = app.dependenciesLoader.andThen(_.map(_.leftMap(f))),
- provideBuilder = app.provideBuilder.andThen(_.map(_.leftMap(f).map(_.map(_.map(_.leftMap(f))))))
+ dependenciesLoader = app.depsLoader.map(_.leftMap(f)),
+ provideBuilder = app.servicesBuilder.andThen(_.map(_.leftMap(f).map(_.map(_.map(_.leftMap(f))))))
)
- def onFailure_(
- f: app.Resourced[FAILURE] => F[Unit]
- ): App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
- app._updateFailureHandlerLoader(appRes =>
- _.onFailure(failure => f(app.Resourced(appRes, failure)) >> app.failureHandlerLoader(appRes).onFailureF(failure))
- )
-
- def onFailure(
- f: app.Resourced[FAILURE] => F[OnFailureBehaviour]
- ): App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
- app._updateFailureHandlerLoader(appRes => _.onFailure(f.compose(app.Resourced(appRes, _))))
-
- def handleFailureWith(
- f: app.Resourced[FAILURE] => F[FAILURE \/ Unit]
- ): App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
- app._updateFailureHandlerLoader(appRes => _.handleFailureWith(f.compose(app.Resourced(appRes, _))))
-
- // compile and run
- def compile(appArgs: List[String] = Nil)(implicit c: AppInterpreter[F]): Resource[F, FAILURE \/ F[NonEmptyList[FAILURE] \/ Unit]] =
- c.compile(appArgs, app)
-
- def run(appArgs: List[String] = Nil)(implicit c: AppInterpreter[F]): F[ExitCode] =
- runMap[ExitCode](appArgs).apply {
- case Left(_) => ExitCode.Error
- case Right(_) => ExitCode.Success
- }
+ inline def onFailure_(
+ f: app.ContextNoDeps ?=> FAILURE => F[Unit]
+ ): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
+ _updateFailureHandlerLoader(_.onFailure(failure => f(failure) >> app.failureHandlerLoader.onFailureF(failure)))
- def runReduce[B](appArgs: List[String] = Nil, f: FAILURE \/ Unit => B)(implicit
- c: AppInterpreter[F],
- semigroup: Semigroup[FAILURE]
- ): F[B] =
- runMap[FAILURE \/ Unit](appArgs)
- .apply {
- case Left(failures) => Left(failures.reduce)
- case Right(_) => Right(())
- }
- .map(f)
+ inline def onFailure(
+ f: app.ContextNoDeps ?=> FAILURE => F[OnFailureBehaviour]
+ ): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
+ _updateFailureHandlerLoader(_.onFailure(f))
- def runRaw(appArgs: List[String] = Nil)(implicit c: AppInterpreter[F]): F[NonEmptyList[FAILURE] \/ Unit] =
- runMap[NonEmptyList[FAILURE] \/ Unit](appArgs)(identity)
+ inline def handleFailureWith(
+ f: app.ContextNoDeps ?=> FAILURE => F[FAILURE \/ Unit]
+ ): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
+ _updateFailureHandlerLoader(_.handleFailureWith(f))
- def runMap[B](appArgs: List[String] = Nil)(f: NonEmptyList[FAILURE] \/ Unit => B)(implicit c: AppInterpreter[F]): F[B] =
- c.run[B](
- compile(appArgs).map {
- case Left(failure) => f(Left(NonEmptyList.one(failure))).pure[F]
- case Right(appLogic) => appLogic.map(f)
- }
- )
- }
-
- implicit class AppThrowOps[F[+_]: Async: Parallel, APP_INFO <: SimpleAppInfo[?], LOGGER_T[
- _[_]
- ]: LoggerAdapter, CONFIG: Show, RESOURCES, DEPENDENCIES](
- app: App[F, Throwable, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES]
- ) {
-
- def compile(appArgs: List[String] = Nil)(implicit c: AppInterpreter[F]): Resource[F, F[Unit]] =
- c.compile(appArgs, app).flatMap {
- case Left(failure) =>
- Resource.raiseError(failure)
- case Right(value) =>
- Resource.pure(value.flatMap {
- case Left(failures: NonEmptyList[Throwable]) =>
- MultiException.fromNel(failures).raiseError[F, Unit]
- case Right(value) => value.pure[F]
- })
- }
-
- def run_(implicit c: AppInterpreter[F]): F[Unit] =
- run().void
-
- def run(appArgs: List[String] = Nil)(implicit c: AppInterpreter[F]): F[ExitCode] =
- c.run(compile(appArgs)).as(ExitCode.Success)
- }
-}
+ private def _updateFailureHandlerLoader(
+ fh: AppContext.NoDeps[INFO, LOGGER_T[F], CONFIG, RESOURCES] ?=> Endo[FailureHandler[F, FAILURE]]
+ ): App[
+ F,
+ FAILURE,
+ INFO,
+ LOGGER_T,
+ CONFIG,
+ RESOURCES,
+ DEPENDENCIES
+ ] = app.copyWith(failureHandlerLoader = fh(app.failureHandlerLoader))
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppArgs.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppArgs.scala
index 0eb4951..89be574 100644
--- a/core/src/main/scala/com/geirolz/app/toolkit/AppArgs.scala
+++ b/core/src/main/scala/com/geirolz/app/toolkit/AppArgs.scala
@@ -5,42 +5,42 @@ import com.geirolz.app.toolkit.ArgDecoder.{ArgDecodingError, MissingArgAtIndex,
import scala.util.Try
-final case class AppArgs(private val value: List[String]) extends AnyVal {
+final case class AppArgs(private val value: List[String]) extends AnyVal:
- def exists(p: AppArgs => Boolean, pN: AppArgs => Boolean*): Boolean =
+ inline def exists(p: AppArgs => Boolean, pN: AppArgs => Boolean*): Boolean =
(p +: pN).forall(_.apply(this))
- def stringAtOrThrow(idx: Int): String =
+ inline def stringAtOrThrow(idx: Int): String =
atOrThrow[String](idx)
- def stringAt(idx: Int): Either[ArgDecodingError, String] =
+ inline def stringAt(idx: Int): Either[ArgDecodingError, String] =
at[String](idx)
- def atOrThrow[V: ArgDecoder](idx: Int): V =
+ inline def atOrThrow[V: ArgDecoder](idx: Int): V =
orThrow(at(idx))
- def at[V: ArgDecoder](idx: Int): Either[ArgDecodingError, V] =
+ inline def at[V: ArgDecoder](idx: Int): Either[ArgDecodingError, V] =
if (isDefinedAt(idx))
ArgDecoder[V].decode(value(idx))
else
Left(MissingArgAtIndex(idx))
- def hasNotFlags(flag1: String, flagN: String*): Boolean =
+ inline def hasNotFlags(flag1: String, flagN: String*): Boolean =
!hasFlags(flag1, flagN*)
- def hasFlags(flag1: String, flagN: String*): Boolean =
+ inline def hasFlags(flag1: String, flagN: String*): Boolean =
(flag1 +: flagN).forall(value.contains(_))
- def hasNotVar(name: String, separator: String = "="): Boolean =
+ inline def hasNotVar(name: String, separator: String = "="): Boolean =
!hasVar(name, separator)
- def hasVar(name: String, separator: String = "="): Boolean =
+ inline def hasVar(name: String, separator: String = "="): Boolean =
getStringVar(name, separator).isRight
- def getStringVar(name: String, separator: String = "="): Either[ArgDecodingError, String] =
+ inline def getStringVar(name: String, separator: String = "="): Either[ArgDecodingError, String] =
getVar[String](name, separator)
- def getVarOrThrow[V: ArgDecoder](name: String, separator: String = "="): V =
+ inline def getVarOrThrow[V: ArgDecoder](name: String, separator: String = "="): V =
orThrow(getVar(name, separator))
def getVar[V: ArgDecoder](name: String, separator: String = "="): Either[ArgDecodingError, V] = {
@@ -50,7 +50,7 @@ final case class AppArgs(private val value: List[String]) extends AnyVal {
}
}
- def toMap(separator: String = "="): Map[String, String] =
+ inline def toMap(separator: String = "="): Map[String, String] =
toTuples(separator).toMap
def toTuples(separator: String = "="): List[(String, String)] =
@@ -58,60 +58,58 @@ final case class AppArgs(private val value: List[String]) extends AnyVal {
(key, value)
}
- def toList[V: ArgDecoder]: List[String] = value
+ inline def toList: List[String] =
+ value
- def isEmpty: Boolean = value.isEmpty
+ inline def isEmpty: Boolean =
+ value.isEmpty
- def isDefinedAt(idx: Int): Boolean =
+ inline def isDefinedAt(idx: Int): Boolean =
value.isDefinedAt(idx)
override def toString: String = s"AppArgs(${value.mkString(", ")})"
- private def orThrow[T](result: Either[ArgDecodingError, T]): T =
+ private inline def orThrow[T](result: Either[ArgDecodingError, T]): T =
result.fold(e => throw e.toException, identity)
-}
-object AppArgs {
- def fromList(args: List[String]): AppArgs =
- AppArgs(args)
+object AppArgs:
+ inline def fromList(args: List[String]): AppArgs = AppArgs(args)
+ given Show[AppArgs] = Show.fromToString
- implicit val show: Show[AppArgs] = Show.fromToString
-}
-
-trait ArgDecoder[T] {
+// ---------------------------------
+trait ArgDecoder[T]:
def decode(value: String): Either[ArgDecodingError, T]
-}
-object ArgDecoder {
- def apply[T: ArgDecoder]: ArgDecoder[T] = implicitly[ArgDecoder[T]]
+object ArgDecoder:
+
+ inline def apply[T: ArgDecoder]: ArgDecoder[T] =
+ summon[ArgDecoder[T]]
- def fromTry[T](t: String => Try[T]): ArgDecoder[T] =
+ inline def fromTry[T](t: String => Try[T]): ArgDecoder[T] =
(value: String) => t(value).toEither.left.map(ArgDecodingException(_))
- sealed trait ArgDecodingError {
- def toException = new RuntimeException(toString)
+ sealed trait ArgDecodingError:
+ inline def toException = new RuntimeException(toString)
final override def toString: String = Show[ArgDecodingError].show(this)
- }
- object ArgDecodingError {
- implicit val show: Show[ArgDecodingError] = {
+
+ object ArgDecodingError:
+ given Show[ArgDecodingError] =
case ArgDecodingException(cause) => s"ArgDecodingException(${cause.getMessage})"
case MissingVariable(name) => s"Missing variable $name"
case MissingArgAtIndex(idx) => s"Missing argument at index $idx"
- }
- }
+
case class ArgDecodingException(cause: Throwable) extends ArgDecodingError
case class MissingVariable(name: String) extends ArgDecodingError
case class MissingArgAtIndex(idx: Int) extends ArgDecodingError
- implicit val stringDecoder: ArgDecoder[String] = s => Right(s)
- implicit val charDecoder: ArgDecoder[Char] = fromTry(s => Try(s.head))
- implicit val byteDecoder: ArgDecoder[Byte] = fromTry(s => Try(s.toByte))
- implicit val shortDecoder: ArgDecoder[Short] = fromTry(s => Try(s.toShort))
- implicit val intDecoder: ArgDecoder[Int] = fromTry(s => Try(s.toInt))
- implicit val longDecoder: ArgDecoder[Long] = fromTry(s => Try(s.toLong))
- implicit val floatDecoder: ArgDecoder[Float] = fromTry(s => Try(s.toFloat))
- implicit val doubleDecoder: ArgDecoder[Double] = fromTry(s => Try(s.toDouble))
- implicit val booleanDecoder: ArgDecoder[Boolean] = fromTry(s => Try(s.toBoolean))
- implicit val bigIntDecoder: ArgDecoder[BigInt] = fromTry(s => Try(BigInt(s)))
- implicit val bigDecimalDecoder: ArgDecoder[BigDecimal] = fromTry(s => Try(BigDecimal(s)))
-}
+ given ArgDecoder[String] = s => Right(s)
+ given ArgDecoder[Char] = fromTry(s => Try(s.head))
+ given ArgDecoder[Byte] = fromTry(s => Try(s.toByte))
+ given ArgDecoder[Short] = fromTry(s => Try(s.toShort))
+ given ArgDecoder[Int] = fromTry(s => Try(s.toInt))
+ given ArgDecoder[Long] = fromTry(s => Try(s.toLong))
+ given ArgDecoder[Float] = fromTry(s => Try(s.toFloat))
+ given ArgDecoder[Double] = fromTry(s => Try(s.toDouble))
+ given ArgDecoder[Boolean] = fromTry(s => Try(s.toBoolean))
+ given ArgDecoder[BigInt] = fromTry(s => Try(BigInt(s)))
+ given ArgDecoder[BigDecimal] = fromTry(s => Try(BigDecimal(s)))
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala
new file mode 100644
index 0000000..46d1838
--- /dev/null
+++ b/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala
@@ -0,0 +1,267 @@
+package com.geirolz.app.toolkit
+
+import cats.effect.{Async, Resource}
+import cats.syntax.all.given
+import cats.{Endo, Parallel, Show}
+import com.geirolz.app.toolkit
+import com.geirolz.app.toolkit.App.*
+import com.geirolz.app.toolkit.AppBuilder.SelectResAndDeps
+import com.geirolz.app.toolkit.failure.FailureHandler
+import com.geirolz.app.toolkit.logger.Logger.Level
+import com.geirolz.app.toolkit.logger.{ConsoleLogger, Logger, LoggerAdapter, NoopLogger}
+import com.geirolz.app.toolkit.novalues.NoFailure.NotNoFailure
+import com.geirolz.app.toolkit.novalues.{NoConfig, NoDependencies, NoFailure, NoResources}
+
+import scala.reflect.ClassTag
+
+final class AppBuilder[F[+_]: Async: Parallel, FAILURE: ClassTag]:
+
+ def withInfo[INFO <: SimpleAppInfo[?]](
+ appInfo: INFO
+ ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, NoopLogger, NoConfig, NoResources] =
+ new AppBuilder.SelectResAndDeps[F, FAILURE, INFO, NoopLogger, NoConfig, NoResources](
+ info = appInfo,
+ messages = AppMessages.default(appInfo),
+ loggerBuilder = NoopLogger[F].pure[F],
+ configLoader = Resource.pure(NoConfig.value),
+ resourcesLoader = Resource.pure(NoResources.value)
+ )
+
+object AppBuilder:
+
+ type Simple[F[+_]] = AppBuilder[F, NoFailure]
+
+ inline def simple[F[+_]: Async: Parallel]: AppBuilder.Simple[F] =
+ new AppBuilder[F, NoFailure]
+
+ inline def withFailure[F[+_]: Async: Parallel, FAILURE: ClassTag: NotNoFailure]: AppBuilder[F, FAILURE] =
+ new AppBuilder[F, FAILURE]
+
+ final class SelectResAndDeps[
+ F[+_]: Async: Parallel,
+ FAILURE: ClassTag,
+ INFO <: SimpleAppInfo[?],
+ LOGGER_T[_[_]]: LoggerAdapter,
+ CONFIG: Show,
+ RESOURCES
+ ] private[AppBuilder] (
+ info: INFO,
+ messages: AppMessages,
+ loggerBuilder: F[LOGGER_T[F]],
+ configLoader: Resource[F, CONFIG],
+ resourcesLoader: Resource[F, RESOURCES]
+ ):
+
+ // ------- MESSAGES -------
+ inline def withMessages(messages: AppMessages): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES] =
+ updateMessages(_ => messages)
+
+ inline def updateMessages(f: Endo[AppMessages]): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES] =
+ copyWith(messages = f(this.messages))
+
+ // ------- LOGGER -------
+ inline def withNoopLogger: AppBuilder.SelectResAndDeps[F, FAILURE, INFO, NoopLogger, CONFIG, RESOURCES] =
+ withLoggerPure(logger = Logger.noop[F])
+
+ inline def withConsoleLogger(minLevel: Level = Level.Info): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, ConsoleLogger, CONFIG, RESOURCES] =
+ withLoggerPure(logger = ConsoleLogger[F](info, minLevel))
+
+ inline def withLoggerPure[LOGGER_T2[_[_]]: LoggerAdapter](
+ logger: LOGGER_T2[F]
+ ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T2, CONFIG, RESOURCES] =
+ withLoggerPure[LOGGER_T2](f = (_: INFO) => logger)
+
+ inline def withLoggerPure[LOGGER_T2[_[_]]: LoggerAdapter](
+ f: INFO => LOGGER_T2[F]
+ ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T2, CONFIG, RESOURCES] =
+ withLogger(f = appInfo => f(appInfo).pure[F])
+
+ // TODO: Add failure
+ inline def withLogger[LOGGER_T2[_[_]]: LoggerAdapter](
+ f: INFO => F[LOGGER_T2[F]]
+ ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T2, CONFIG, RESOURCES] =
+ copyWith(loggerBuilder = f(info))
+
+ // ------- CONFIG -------
+ inline def withoutConfig: AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, NoConfig, RESOURCES] =
+ withConfigPure[NoConfig](NoConfig.value)
+
+ inline def withConfigPure[CONFIG2: Show](
+ config: CONFIG2
+ ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG2, RESOURCES] =
+ withConfigF(config.pure[F])
+
+ // TODO: Add failure
+ inline def withConfigF[CONFIG2: Show](
+ configLoader: INFO => F[CONFIG2]
+ )(using DummyImplicit): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG2, RESOURCES] =
+ withConfig(i => Resource.eval(configLoader(i)))
+
+ // TODO: Add failure
+ inline def withConfigF[CONFIG2: Show](
+ configLoader: F[CONFIG2]
+ ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG2, RESOURCES] =
+ withConfig(Resource.eval(configLoader))
+
+ // TODO: Add failure
+ inline def withConfig[CONFIG2: Show](
+ configLoader: Resource[F, CONFIG2]
+ ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG2, RESOURCES] =
+ withConfig(_ => configLoader)
+
+ // TODO: Add failure
+ inline def withConfig[CONFIG2: Show](
+ configLoader: INFO => Resource[F, CONFIG2]
+ ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG2, RESOURCES] =
+ copyWith(configLoader = configLoader(this.info))
+
+ // ------- RESOURCES -------
+ inline def withoutResources: AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG, NoResources] =
+ withResources[NoResources](Resource.pure(NoResources.value))
+
+ // TODO: Add failure
+ /** Resources are loaded into context and released before providing the services. */
+ inline def withResources[RESOURCES2](
+ resourcesLoader: Resource[F, RESOURCES2]
+ ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES2] =
+ copyWith(resourcesLoader = resourcesLoader)
+
+ // ------- DEPENDENCIES -------
+ inline def withoutDependencies: AppBuilder.SelectProvide[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, NoDependencies] =
+ dependsOn[NoDependencies, FAILURE](Resource.pure(NoDependencies.value))
+
+ /** Dependencies are loaded into context and released at the end of the application. */
+ inline def dependsOn[DEPENDENCIES, FAILURE2 <: FAILURE: ClassTag](
+ f: AppContext.NoDeps[INFO, LOGGER_T[F], CONFIG, RESOURCES] ?=> Resource[F, FAILURE2 | DEPENDENCIES]
+ ): AppBuilder.SelectProvide[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
+ dependsOnE[DEPENDENCIES, FAILURE2](f.map {
+ case deps: DEPENDENCIES => Right(deps)
+ case failure: FAILURE2 => Left(failure)
+ })
+
+ /** Dependencies are loaded into context and released at the end of the application. */
+ def dependsOnE[DEPENDENCIES, FAILURE2 <: FAILURE](
+ f: AppContext.NoDeps[INFO, LOGGER_T[F], CONFIG, RESOURCES] ?=> Resource[F, FAILURE2 \/ DEPENDENCIES]
+ )(using DummyImplicit): AppBuilder.SelectProvide[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
+ AppBuilder.SelectProvide(
+ info = info,
+ messages = messages,
+ loggerBuilder = loggerBuilder,
+ configLoader = configLoader,
+ resourcesLoader = resourcesLoader,
+ dependenciesLoader = f(using _),
+ beforeProvidingTask = _ => ().pure[F]
+ )
+
+ private def copyWith[
+ G[+_]: Async: Parallel,
+ FAILURE2: ClassTag,
+ INFO2 <: SimpleAppInfo[?],
+ LOGGER_T2[_[_]]: LoggerAdapter,
+ CONFIG2: Show,
+ RESOURCES2
+ ](
+ info: INFO2 = this.info,
+ messages: AppMessages = this.messages,
+ loggerBuilder: G[LOGGER_T2[G]] = this.loggerBuilder,
+ configLoader: Resource[G, CONFIG2] = this.configLoader,
+ resourcesLoader: Resource[G, RESOURCES2] = this.resourcesLoader
+ ) = new AppBuilder.SelectResAndDeps[G, FAILURE2, INFO2, LOGGER_T2, CONFIG2, RESOURCES2](
+ info = info,
+ messages = messages,
+ loggerBuilder = loggerBuilder,
+ configLoader = configLoader,
+ resourcesLoader = resourcesLoader
+ )
+
+ final case class SelectProvide[
+ F[+_]: Async: Parallel,
+ FAILURE,
+ INFO <: SimpleAppInfo[?],
+ LOGGER_T[_[_]]: LoggerAdapter,
+ CONFIG: Show,
+ RESOURCES,
+ DEPENDENCIES
+ ](
+ info: INFO,
+ messages: AppMessages,
+ loggerBuilder: F[LOGGER_T[F]],
+ configLoader: Resource[F, CONFIG],
+ resourcesLoader: Resource[F, RESOURCES],
+ dependenciesLoader: AppContext.NoDeps[INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, FAILURE \/ DEPENDENCIES],
+ beforeProvidingTask: AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit]
+ ):
+
+ // ------- BEFORE PROVIDING -------
+ inline def beforeProviding(
+ f: AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] ?=> F[Unit]
+ ): AppBuilder.SelectProvide[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
+ copy(beforeProvidingTask = d => this.beforeProvidingTask(d) >> f(using d))
+
+ // ------- PROVIDE -------
+ def provideOne[FAILURE2 <: FAILURE: ClassTag](
+ f: AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] ?=> F[FAILURE2 | Unit]
+ ): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
+ provideOneE[FAILURE2](f.map {
+ case failure: FAILURE2 => Left(failure)
+ case _: Unit => Right(())
+ })
+
+ inline def provideOneE[FAILURE2 <: FAILURE](
+ f: AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] ?=> F[FAILURE2 \/ Unit]
+ ): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
+ provideParallelE[FAILURE2](List(f))
+
+ inline def provideOneF[FAILURE2 <: FAILURE](
+ f: AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] ?=> F[FAILURE2 \/ F[Unit]]
+ ): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
+ provideParallelAttemptFE[FAILURE2](f.map(_.map(v => List(v.map(_.asRight[FAILURE2])))))
+
+ // provide
+ def provideParallel[FAILURE2 <: FAILURE: ClassTag](
+ f: AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] ?=> List[F[FAILURE2 | Unit]]
+ ): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
+ provideParallelE(f.map(_.map {
+ case failure: FAILURE2 => Left(failure)
+ case _: Unit => Right(())
+ }))
+
+ inline def provideParallelE[FAILURE2 <: FAILURE](
+ f: AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] ?=> List[F[FAILURE2 \/ Unit]]
+ )(using DummyImplicit): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
+ provideParallelFE[FAILURE2](f.pure[F])
+
+ // provideF
+ def provideParallelF[FAILURE2 <: FAILURE: ClassTag](
+ f: AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] ?=> F[List[F[FAILURE2 | Unit]]]
+ )(using DummyImplicit): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
+ provideParallelFE(f.map(_.map(_.map {
+ case failure: FAILURE2 => Left(failure)
+ case _: Unit => Right(())
+ })))
+
+ inline def provideParallelFE[FAILURE2 <: FAILURE](
+ f: AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] ?=> F[List[F[FAILURE2 \/ Unit]]]
+ )(using DummyImplicit): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
+ provideParallelAttemptFE(f.map(Right(_)))
+
+ // TODO Missing the union version
+ def provideParallelAttemptFE[FAILURE2 <: FAILURE](
+ f: AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] ?=> F[FAILURE2 \/ List[F[FAILURE2 \/ Unit]]]
+ ): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
+ // TODO Allow custom AppMessages
+ new App(
+ info = info,
+ messages = messages,
+ failureHandlerLoader = FailureHandler.logAndCancelAll[F, FAILURE](
+ appMessages = ctx.messages,
+ logger = LoggerAdapter[LOGGER_T].toToolkit(ctx.logger)
+ ),
+ loggerBuilder = loggerBuilder,
+ resourcesLoader = resourcesLoader,
+ beforeProvidingTask = beforeProvidingTask,
+ onFinalizeTask = _ => ().pure[F],
+ configLoader = configLoader,
+ depsLoader = dependenciesLoader(ctx),
+ servicesBuilder = f(using _)
+ )
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppInterpreter.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppCompiler.scala
similarity index 51%
rename from core/src/main/scala/com/geirolz/app/toolkit/AppInterpreter.scala
rename to core/src/main/scala/com/geirolz/app/toolkit/AppCompiler.scala
index 12d57a7..01f1c77 100644
--- a/core/src/main/scala/com/geirolz/app/toolkit/AppInterpreter.scala
+++ b/core/src/main/scala/com/geirolz/app/toolkit/AppCompiler.scala
@@ -1,47 +1,44 @@
package com.geirolz.app.toolkit
-import cats.{Parallel, Show}
import cats.data.{EitherT, NonEmptyList}
import cats.effect.implicits.{genSpawnOps, monadCancelOps_}
-import cats.effect.kernel.MonadCancelThrow
import cats.effect.{Async, Fiber, Ref, Resource}
-import com.geirolz.app.toolkit.FailureHandler.OnFailureBehaviour
+import cats.{Parallel, Show}
+import com.geirolz.app.toolkit.AppContext.NoDeps
+import com.geirolz.app.toolkit.failure.FailureHandler.OnFailureBehaviour
import com.geirolz.app.toolkit.logger.LoggerAdapter
+import com.geirolz.app.toolkit.novalues.NoDependencies
-trait AppInterpreter[F[+_]] {
-
- def run[T](compiledApp: Resource[F, F[T]])(implicit F: MonadCancelThrow[F]): F[T]
+trait AppCompiler[F[+_]]:
def compile[
FAILURE,
- APP_INFO <: SimpleAppInfo[?],
+ INFO <: SimpleAppInfo[?],
LOGGER_T[_[_]]: LoggerAdapter,
CONFIG: Show,
RESOURCES,
DEPENDENCIES
- ](appArgs: List[String], app: App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES])(implicit
+ ](appArgs: List[String], app: App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES])(using
F: Async[F],
P: Parallel[F]
): Resource[F, FAILURE \/ F[NonEmptyList[FAILURE] \/ Unit]]
-}
-object AppInterpreter {
- import cats.syntax.all.*
+object AppCompiler:
- def apply[F[+_]](implicit ac: AppInterpreter[F]): AppInterpreter[F] = ac
+ import cats.syntax.all.*
- implicit def default[F[+_]]: AppInterpreter[F] = new AppInterpreter[F] {
+ def apply[F[+_]](using ac: AppCompiler[F]): AppCompiler[F] = ac
- override def run[T](compiledApp: Resource[F, F[T]])(implicit F: MonadCancelThrow[F]): F[T] = compiledApp.useEval
+ given [F[+_]]: AppCompiler[F] = new AppCompiler[F] {
- override def compile[FAILURE, APP_INFO <: SimpleAppInfo[?], LOGGER_T[_[_]]: LoggerAdapter, CONFIG: Show, RESOURCES, DEPENDENCIES](
+ override def compile[FAILURE, INFO <: SimpleAppInfo[?], LOGGER_T[_[_]]: LoggerAdapter, CONFIG: Show, RESOURCES, DEPENDENCIES](
appArgs: List[String],
- app: App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES]
- )(implicit F: Async[F], P: Parallel[F]): Resource[F, FAILURE \/ F[NonEmptyList[FAILURE] \/ Unit]] =
+ app: App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES]
+ )(using F: Async[F], P: Parallel[F]): Resource[F, FAILURE \/ F[NonEmptyList[FAILURE] \/ Unit]] =
(
for {
- // -------------------- RESOURCES-------------------
+ // -------------------- CONTEXT -------------------
// logger
userLogger <- EitherT.right[FAILURE](Resource.eval(app.loggerBuilder))
toolkitLogger = LoggerAdapter[LOGGER_T].toToolkit[F](userLogger)
@@ -50,39 +47,46 @@ object AppInterpreter {
)
// config
- _ <- toolkitResLogger.debug(app.appMessages.loadingConfig)
+ _ <- toolkitResLogger.debug(app.messages.loadingConfig)
appConfig <- EitherT.right[FAILURE](app.configLoader)
- _ <- toolkitResLogger.info(app.appMessages.configSuccessfullyLoaded)
+ _ <- toolkitResLogger.info(app.messages.configSuccessfullyLoaded)
_ <- toolkitResLogger.info(appConfig.show)
- // other resources
- otherResources <- EitherT.right[FAILURE](app.resourcesLoader)
-
// group resources
- appResources: App.Resources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] = App.Resources(
- info = app.appInfo,
- args = AppArgs(appArgs),
- logger = userLogger,
- config = appConfig,
- resources = otherResources
- )
+ given AppContext.NoDeps[INFO, LOGGER_T[F], CONFIG, RESOURCES] <-
+ EitherT.right[FAILURE](
+ Resource.eval(
+ app.resourcesLoader.use(otherResources =>
+ AppContext
+ .noDependencies(
+ info = app.info,
+ messages = app.messages,
+ args = AppArgs(appArgs),
+ logger = userLogger,
+ config = appConfig,
+ resources = otherResources
+ )
+ .pure[F]
+ )
+ )
+ )
// ------------------- DEPENDENCIES -----------------
- _ <- toolkitResLogger.debug(app.appMessages.buildingServicesEnv)
- appDepServices <- EitherT(app.dependenciesLoader(appResources))
- _ <- toolkitResLogger.info(app.appMessages.servicesEnvSuccessfullyBuilt)
- appDependencies = App.Dependencies(appResources, appDepServices)
+ _ <- toolkitResLogger.debug(app.messages.buildingServicesEnv)
+ appDepServices <- EitherT(app.depsLoader)
+ _ <- toolkitResLogger.info(app.messages.servicesEnvSuccessfullyBuilt)
+ appContext = ctx.withDependencies(appDepServices)
// --------------------- SERVICES -------------------
- _ <- toolkitResLogger.debug(app.appMessages.buildingApp)
- appProvServices <- EitherT(Resource.eval(app.provideBuilder(appDependencies)))
- _ <- toolkitResLogger.info(app.appMessages.appSuccessfullyBuilt)
+ _ <- toolkitResLogger.debug(app.messages.buildingApp)
+ appProvServices <- EitherT(Resource.eval(app.servicesBuilder(appContext)))
+ _ <- toolkitResLogger.info(app.messages.appSuccessfullyBuilt)
// --------------------- APP ------------------------
appLogic = for {
fibers <- Ref[F].of(List.empty[Fiber[F, Throwable, Unit]])
failures <- Ref[F].of(List.empty[FAILURE])
- failureHandler = app.failureHandlerLoader(appResources)
+ failureHandler = app.failureHandlerLoader
onFailureTask: (FAILURE => F[Unit]) =
failureHandler
.handleFailureWithF(_)
@@ -112,15 +116,14 @@ object AppInterpreter {
maybeReducedFailures <- failures.get.map(NonEmptyList.fromList(_))
} yield maybeReducedFailures.toLeft(())
} yield {
- toolkitLogger.info(app.appMessages.startingApp) >>
- app.beforeProvidingF(appDependencies) >>
+ toolkitLogger.info(app.messages.startingApp) >>
+ app.beforeProvidingTask(appContext) >>
appLogic
- .onCancel(toolkitLogger.info(app.appMessages.appWasStopped))
- .onError(e => toolkitLogger.error(e)(app.appMessages.appEnErrorOccurred))
+ .onCancel(toolkitLogger.info(app.messages.appWasStopped))
+ .onError(e => toolkitLogger.error(e)(app.messages.appAnErrorOccurred))
.guarantee(
- app.onFinalizeF(appDependencies) >> toolkitLogger.info(app.appMessages.shuttingDownApp)
+ app.onFinalizeTask(appContext) >> toolkitLogger.info(app.messages.shuttingDownApp)
)
}
).value
}
-}
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppContext.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppContext.scala
new file mode 100644
index 0000000..2e711cd
--- /dev/null
+++ b/core/src/main/scala/com/geirolz/app/toolkit/AppContext.scala
@@ -0,0 +1,99 @@
+package com.geirolz.app.toolkit
+
+import cats.syntax.all.given
+import com.geirolz.app.toolkit.novalues.{NoDependencies, NoResources}
+
+final case class AppContext[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, DEPENDENCIES, RESOURCES](
+ info: INFO,
+ messages: AppMessages,
+ args: AppArgs,
+ logger: LOGGER,
+ config: CONFIG,
+ dependencies: DEPENDENCIES,
+ resources: RESOURCES
+) {
+ type AppInfo = INFO
+ type Logger = LOGGER
+ type Config = CONFIG
+ type Dependencies = DEPENDENCIES
+ type Resources = RESOURCES
+
+ def withDependencies[D](newDependencies: D): AppContext[INFO, LOGGER, CONFIG, D, RESOURCES] =
+ AppContext(
+ info = info,
+ messages = messages,
+ args = args,
+ logger = logger,
+ config = config,
+ dependencies = newDependencies,
+ resources = resources
+ )
+
+ override def toString: String =
+ s"""AppContext(
+ | info = $info,
+ | args = $args,
+ | logger = $logger,
+ | config = $config,
+ | dependencies = $dependencies,
+ | resources = $resources
+ |)""".stripMargin
+}
+
+object AppContext:
+
+ type NoDeps[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, RESOURCES] =
+ AppContext[INFO, LOGGER, CONFIG, NoDependencies, RESOURCES]
+
+ type NoDepsAndRes[INFO <: SimpleAppInfo[?], LOGGER, CONFIG] =
+ NoDeps[INFO, LOGGER, CONFIG, NoResources]
+
+ private[toolkit] def noDependencies[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, RESOURCES](
+ info: INFO,
+ messages: AppMessages,
+ args: AppArgs,
+ logger: LOGGER,
+ config: CONFIG,
+ resources: RESOURCES
+ ): NoDeps[INFO, LOGGER, CONFIG, RESOURCES] =
+ apply(
+ info = info,
+ messages = messages,
+ args = args,
+ logger = logger,
+ config = config,
+ dependencies = NoDependencies.value,
+ resources = resources
+ )
+
+ private[toolkit] def apply[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, DEPENDENCIES, RESOURCES](
+ info: INFO,
+ messages: AppMessages,
+ args: AppArgs,
+ logger: LOGGER,
+ config: CONFIG,
+ dependencies: DEPENDENCIES,
+ resources: RESOURCES
+ ): AppContext[INFO, LOGGER, CONFIG, DEPENDENCIES, RESOURCES] =
+ new AppContext[INFO, LOGGER, CONFIG, DEPENDENCIES, RESOURCES](
+ info = info,
+ messages = messages,
+ args = args,
+ logger = logger,
+ config = config,
+ dependencies = dependencies,
+ resources = resources
+ )
+
+ def unapply[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, DEPENDENCIES, RESOURCES](
+ res: AppContext[INFO, LOGGER, CONFIG, DEPENDENCIES, RESOURCES]
+ ): Option[(INFO, AppMessages, AppArgs, LOGGER, CONFIG, DEPENDENCIES, RESOURCES)] =
+ (
+ res.info,
+ res.messages,
+ res.args,
+ res.logger,
+ res.config,
+ res.dependencies,
+ res.resources
+ ).some
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppLogicInterpreter.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppLogicInterpreter.scala
new file mode 100644
index 0000000..c3b7a62
--- /dev/null
+++ b/core/src/main/scala/com/geirolz/app/toolkit/AppLogicInterpreter.scala
@@ -0,0 +1,44 @@
+package com.geirolz.app.toolkit
+
+import cats.{Applicative, MonadThrow}
+import cats.data.NonEmptyList
+import cats.effect.Resource
+import com.geirolz.app.toolkit.novalues.NoFailure
+
+sealed trait AppLogicInterpreter[F[_], R[_], FAILURE]:
+ def interpret[T](appLogic: Resource[F, FAILURE \/ F[NonEmptyList[FAILURE] \/ T]]): Resource[F, F[R[T]]]
+ def isSuccess[T](value: R[T]): Boolean
+
+object AppLogicInterpreter:
+
+ import cats.syntax.all.*
+
+ def apply[F[_], R[_], FAILURE](using
+ i: AppLogicInterpreter[F, R, FAILURE]
+ ): AppLogicInterpreter[F, R, FAILURE] = i
+
+ given [F[_]: MonadThrow]: AppLogicInterpreter[F, [X] =>> X, NoFailure] =
+ new AppLogicInterpreter[F, [X] =>> X, NoFailure]:
+ override def isSuccess[T](value: T): Boolean = true
+ override def interpret[T](appLogic: Resource[F, NoFailure \/ F[NonEmptyList[NoFailure] \/ T]]): Resource[F, F[T]] =
+ appLogic.map {
+ case Left(_) =>
+ MonadThrow[F].raiseError(new RuntimeException("Unreachable point."))
+ case Right(value: F[NonEmptyList[NoFailure] \/ T]) =>
+ value.flatMap {
+ case Left(_) =>
+ MonadThrow[F].raiseError(new RuntimeException("Unreachable point."))
+ case Right(value) =>
+ value.pure[F]
+ }
+ }
+
+ given [F[_]: Applicative, FAILURE]: AppLogicInterpreter[F, Either[NonEmptyList[FAILURE], *], FAILURE] =
+ new AppLogicInterpreter[F, Either[NonEmptyList[FAILURE], *], FAILURE]:
+ override def isSuccess[T](value: NonEmptyList[FAILURE] \/ T): Boolean = value.isRight
+ override def interpret[T](
+ appLogic: Resource[F, FAILURE \/ F[NonEmptyList[FAILURE] \/ T]]
+ ): Resource[F, F[NonEmptyList[FAILURE] \/ T]] = appLogic.map {
+ case Left(failure) => Left(NonEmptyList.one(failure)).pure[F]
+ case Right(value) => value
+ }
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppMessages.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppMessages.scala
index 7202749..c92ee87 100644
--- a/core/src/main/scala/com/geirolz/app/toolkit/AppMessages.scala
+++ b/core/src/main/scala/com/geirolz/app/toolkit/AppMessages.scala
@@ -9,27 +9,30 @@ case class AppMessages(
appSuccessfullyBuilt: String,
startingApp: String,
appWasStopped: String,
- appEnErrorOccurred: String,
+ appAnErrorOccurred: String,
+ appAFailureOccurred: String,
shuttingDownApp: String
)
-object AppMessages {
- def fromAppInfo[APP_INFO <: SimpleAppInfo[?]](info: APP_INFO)(
- f: APP_INFO => AppMessages
+object AppMessages:
+
+ inline def fromAppInfo[INFO <: SimpleAppInfo[?]](info: INFO)(
+ f: INFO => AppMessages
): AppMessages = f(info)
- def default(info: SimpleAppInfo[?]): AppMessages = AppMessages.fromAppInfo(info)(info =>
- AppMessages(
- loadingConfig = "Loading configuration...",
- configSuccessfullyLoaded = "Configuration successfully loaded.",
- buildingServicesEnv = "Building services environment...",
- servicesEnvSuccessfullyBuilt = "Services environment successfully built.",
- buildingApp = "Building App...",
- appSuccessfullyBuilt = "App successfully built.",
- startingApp = s"Starting ${info.buildRefName}...",
- appWasStopped = s"${info.name} was stopped.",
- appEnErrorOccurred = s"${info.name} was stopped due an error.",
- shuttingDownApp = s"Shutting down ${info.name}..."
+ def default(info: SimpleAppInfo[?]): AppMessages =
+ fromAppInfo(info)(info =>
+ AppMessages(
+ loadingConfig = "Loading configuration...",
+ configSuccessfullyLoaded = "Configuration successfully loaded.",
+ buildingServicesEnv = "Building services environment...",
+ servicesEnvSuccessfullyBuilt = "Services environment successfully built.",
+ buildingApp = "Building App...",
+ appSuccessfullyBuilt = "App successfully built.",
+ startingApp = s"Starting ${info.buildRefName}...",
+ appWasStopped = s"${info.name} was stopped.",
+ appAnErrorOccurred = s"Error occurred.",
+ appAFailureOccurred = s"Failure occurred.",
+ shuttingDownApp = s"Shutting down ${info.name}..."
+ )
)
- )
-}
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/FailureHandler.scala b/core/src/main/scala/com/geirolz/app/toolkit/FailureHandler.scala
deleted file mode 100644
index 14c64a2..0000000
--- a/core/src/main/scala/com/geirolz/app/toolkit/FailureHandler.scala
+++ /dev/null
@@ -1,85 +0,0 @@
-package com.geirolz.app.toolkit
-
-import cats.{~>, Applicative, Functor}
-import cats.data.NonEmptyList
-import com.geirolz.app.toolkit.FailureHandler.OnFailureBehaviour
-
-case class FailureHandler[F[_], FAILURE](
- onFailureF: FAILURE => F[OnFailureBehaviour],
- handleFailureWithF: FAILURE => F[FAILURE \/ Unit]
-) { $this =>
-
- def onFailure(f: FAILURE => F[OnFailureBehaviour]): FailureHandler[F, FAILURE] =
- copy(onFailureF = f)
-
- def handleFailureWith(f: FAILURE => F[FAILURE \/ Unit]): FailureHandler[F, FAILURE] =
- copy(handleFailureWithF = f)
-
- def mapK[G[_]](f: F ~> G): FailureHandler[G, FAILURE] =
- FailureHandler[G, FAILURE](
- onFailureF = (e: FAILURE) => f($this.onFailureF(e)),
- handleFailureWithF = (e: FAILURE) => f($this.handleFailureWithF(e))
- )
-
- def widen[EE <: FAILURE]: FailureHandler[F, EE] =
- this.asInstanceOf[FailureHandler[F, EE]]
-
- def widenNel[EE](implicit
- env: FAILURE =:= NonEmptyList[EE]
- ): FailureHandler[F, NonEmptyList[EE]] =
- this.asInstanceOf[FailureHandler[F, NonEmptyList[EE]]]
-}
-object FailureHandler extends FailureHandlerSyntax {
-
- def summon[F[_], E](implicit ev: FailureHandler[F, E]): FailureHandler[F, E] = ev
-
- def cancelAll[F[_]: Applicative, FAILURE]: FailureHandler[F, FAILURE] =
- FailureHandler[F, FAILURE](
- onFailureF = (_: FAILURE) => Applicative[F].pure(OnFailureBehaviour.CancelAll),
- handleFailureWithF = (e: FAILURE) => Applicative[F].pure(Left(e))
- )
-
- sealed trait OnFailureBehaviour
- object OnFailureBehaviour {
- case object CancelAll extends OnFailureBehaviour
- case object DoNothing extends OnFailureBehaviour
- }
-}
-sealed trait FailureHandlerSyntax {
-
- import cats.syntax.all.*
-
- implicit class FailureHandlerOps[F[+_], FAILURE]($this: FailureHandler[F, FAILURE]) {
- final def liftNonEmptyList(implicit
- F: Applicative[F]
- ): FailureHandler[F, NonEmptyList[FAILURE]] =
- FailureHandler[F, NonEmptyList[FAILURE]](
- onFailureF = (failures: NonEmptyList[FAILURE]) =>
- failures
- .traverse($this.onFailureF(_))
- .map(
- _.collectFirst { case OnFailureBehaviour.CancelAll =>
- OnFailureBehaviour.CancelAll
- }.getOrElse(OnFailureBehaviour.DoNothing)
- ),
- handleFailureWithF = (failures: NonEmptyList[FAILURE]) =>
- failures.toList
- .traverse($this.handleFailureWithF(_))
- .map(_.partitionEither(identity)._1.toNel)
- .map {
- case None => ().asRight[NonEmptyList[FAILURE]]
- case Some(nelE) => nelE.asLeft[Unit]
- }
- )
- }
-
- implicit class FailureHandlerNelOps[F[+_], FAILURE](
- $this: FailureHandler[F, NonEmptyList[FAILURE]]
- ) {
- final def single(implicit F: Functor[F]): FailureHandler[F, FAILURE] =
- FailureHandler[F, FAILURE](
- onFailureF = (e: FAILURE) => $this.onFailureF(NonEmptyList.one(e)),
- handleFailureWithF = (e: FAILURE) => $this.handleFailureWithF(NonEmptyList.one(e)).map(_.leftMap(_.head))
- )
- }
-}
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/IOApp.scala b/core/src/main/scala/com/geirolz/app/toolkit/IOApp.scala
new file mode 100644
index 0000000..e5169a1
--- /dev/null
+++ b/core/src/main/scala/com/geirolz/app/toolkit/IOApp.scala
@@ -0,0 +1,9 @@
+package com.geirolz.app.toolkit
+
+import cats.effect.{ExitCode, IO, IOApp}
+
+object IOApp:
+ trait Toolkit extends IOApp:
+ export com.geirolz.app.toolkit.ctx
+ val app: App[IO, ?, ?, ?, ?, ?, ?]
+ def run(args: List[String]): IO[ExitCode] = app.run(args)
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/SimpleAppInfo.scala b/core/src/main/scala/com/geirolz/app/toolkit/SimpleAppInfo.scala
index 1fe8806..ecd17c2 100644
--- a/core/src/main/scala/com/geirolz/app/toolkit/SimpleAppInfo.scala
+++ b/core/src/main/scala/com/geirolz/app/toolkit/SimpleAppInfo.scala
@@ -5,15 +5,15 @@ import cats.implicits.showInterpolator
import java.time.LocalDateTime
-trait SimpleAppInfo[T] {
+trait SimpleAppInfo[T]:
val name: T
val version: T
val scalaVersion: T
val sbtVersion: T
val buildRefName: T
val builtOn: LocalDateTime
-}
-object SimpleAppInfo {
+
+object SimpleAppInfo:
def apply[T: Show](
name: T,
@@ -74,8 +74,6 @@ object SimpleAppInfo {
val builtOn: LocalDateTime
) extends SimpleAppInfo[T]
- def genRefNameString[T: Show](name: T, version: T, builtOn: LocalDateTime): String = {
- implicit val showLocalDataTime: Show[LocalDateTime] = Show.fromToString[LocalDateTime]
+ def genRefNameString[T: Show](name: T, version: T, builtOn: LocalDateTime): String =
+ given Show[LocalDateTime] = Show.fromToString[LocalDateTime]
show"$name:$version-$builtOn"
- }
-}
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/TypeInequalities.scala b/core/src/main/scala/com/geirolz/app/toolkit/TypeInequalities.scala
deleted file mode 100644
index 2c17c4c..0000000
--- a/core/src/main/scala/com/geirolz/app/toolkit/TypeInequalities.scala
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.geirolz.app.toolkit
-
-import scala.annotation.implicitNotFound
-
-//noinspection ScalaFileName
-// $COVERAGE-OFF$
-@implicitNotFound(msg = "Cannot prove that ${A} =:!= ${B}.")
-sealed trait =:!=[A, B]
-
-//noinspection ScalaFileName
-object =:!= {
- implicit def neq[A, B]: A =:!= B = new =:!=[A, B] {}
- implicit def neqAmbig1[A]: A =:!= A = null
- implicit def neqAmbig2[A]: A =:!= A = null
-}
-// $COVERAGE-ON$
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/console/AnsiValue.scala b/core/src/main/scala/com/geirolz/app/toolkit/console/AnsiValue.scala
index 1caf0b0..22f0b52 100644
--- a/core/src/main/scala/com/geirolz/app/toolkit/console/AnsiValue.scala
+++ b/core/src/main/scala/com/geirolz/app/toolkit/console/AnsiValue.scala
@@ -24,31 +24,42 @@ import com.geirolz.app.toolkit.console.AnsiValue.AnsiText
* .withBackground(AnsiValue.B.BLACK)
* .withStyle(AnsiValue.S.BLINK)
* }}}
+ *
+ *
Foreground | | Background |
BLACK | | BLACK_B |
RED | | RED_B |
GREEN | | GREEN_B |
YELLOW | | YELLOW_B |
BLUE | | BLUE_B |
MAGENTA | | MAGENTA_B |
CYAN | | CYAN_B |
WHITE | | WHITE_B |
*/
-sealed trait AnsiValue {
+sealed trait AnsiValue:
val value: String
- def apply[T](msg: T)(implicit s: Show[T] = Show.fromToString[T]): AnsiText =
+ def apply[T](msg: T)(using s: Show[T] = Show.fromToString[T]): AnsiText =
show"$value$msg${AnsiValue.S.RESET}"
- lazy val foreground: AnsiValue.F = this match {
- case AnsiValue.Rich(fg, _, _) => fg
- case bg: AnsiValue.F => bg
- case _ => AnsiValue.F.NONE
- }
+ lazy val foreground: AnsiValue.F =
+ this match
+ case AnsiValue.Rich(fg, _, _) => fg
+ case bg: AnsiValue.F => bg
+ case _ => AnsiValue.F.NONE
- lazy val background: AnsiValue.B = this match {
- case AnsiValue.Rich(_, bg, _) => bg
- case bg: AnsiValue.B => bg
- case _ => AnsiValue.B.NONE
- }
+ lazy val background: AnsiValue.B =
+ this match
+ case AnsiValue.Rich(_, bg, _) => bg
+ case bg: AnsiValue.B => bg
+ case _ => AnsiValue.B.NONE
- lazy val style: AnsiValue.S = this match {
- case AnsiValue.Rich(_, _, s) => s
- case s: AnsiValue.S => s
- case _ => AnsiValue.S.NONE
- }
+ lazy val style: AnsiValue.S =
+ this match
+ case AnsiValue.Rich(_, _, s) => s
+ case s: AnsiValue.S => s
+ case _ => AnsiValue.S.NONE
def withForeground(fg: AnsiValue.F): AnsiValue =
withValue(fg)
@@ -69,7 +80,7 @@ sealed trait AnsiValue {
withStyle(AnsiValue.S.NONE)
def withValue(value: AnsiValue): AnsiValue =
- (this, value) match {
+ (this, value) match
case (_: AnsiValue.F, b: AnsiValue.F) => b
case (_: AnsiValue.B, b: AnsiValue.B) => b
case (_: AnsiValue.S, b: AnsiValue.S) => b
@@ -77,11 +88,10 @@ sealed trait AnsiValue {
case (a: AnsiValue.Rich, b: AnsiValue) => a.withEvalValue(b)
case (a, b: AnsiValue.Rich) => b.withEvalValue(a)
case (a, b) => AnsiValue.Rich().withEvalValue(a).withEvalValue(b)
- }
override def toString: String = value
-}
-object AnsiValue extends AnsiValueInstances with AnsiValueSyntax {
+
+object AnsiValue extends AnsiValueInstances with AnsiValueSyntax:
type AnsiText = String
@@ -99,29 +109,26 @@ object AnsiValue extends AnsiValueInstances with AnsiValueSyntax {
fg: AnsiValue.F,
bg: AnsiValue.B,
s: AnsiValue.S
- ) extends AnsiValue {
+ ) extends AnsiValue:
- private[AnsiValue] def withEvalValue(value: AnsiValue): AnsiValue.Rich = {
- value match {
+ private[AnsiValue] def withEvalValue(value: AnsiValue): AnsiValue.Rich =
+ value match
case value: AnsiValue.Rich => value
case value: AnsiValue.F => copy(fg = value)
case value: AnsiValue.B => copy(bg = value)
case value: AnsiValue.S => copy(s = value)
- }
- }
override val value: AnsiText = List(s, bg, fg).mkString
- }
- object Rich {
+
+ object Rich:
private[AnsiValue] def apply(
foreground: AnsiValue.F = AnsiValue.F.NONE,
background: AnsiValue.B = AnsiValue.B.NONE,
style: AnsiValue.S = AnsiValue.S.NONE
): AnsiValue.Rich = new Rich(foreground, background, style)
- }
case class F(value: String) extends AnsiValue
- object F {
+ object F:
private[F] def apply(value: String): AnsiValue.F =
new F(value)
@@ -140,6 +147,12 @@ object AnsiValue extends AnsiValueInstances with AnsiValueSyntax {
*/
final val RED: AnsiValue.F = F(scala.Console.RED)
+ /** Foreground color for ANSI Bright Red
+ *
+ * @group color-bright-red
+ */
+ final val BRIGHT_RED: AnsiValue.F = F("\u001b[91m")
+
/** Foreground color for ANSI green
*
* @group color-green
@@ -175,10 +188,9 @@ object AnsiValue extends AnsiValueInstances with AnsiValueSyntax {
* @group color-white
*/
final val WHITE: AnsiValue.F = F(scala.Console.WHITE)
- }
case class B(value: String) extends AnsiValue
- object B {
+ object B:
private[B] def apply(value: String): AnsiValue.B =
new B(value)
@@ -197,6 +209,12 @@ object AnsiValue extends AnsiValueInstances with AnsiValueSyntax {
*/
final val RED = B(scala.Console.RED_B)
+ /** Background color for ANSI Bright Red
+ *
+ * @group color-bright-red
+ */
+ final val BRIGHT_RED: AnsiValue.B = B("\u001b[101m")
+
/** Background color for ANSI green
*
* @group color-green
@@ -232,10 +250,9 @@ object AnsiValue extends AnsiValueInstances with AnsiValueSyntax {
* @group color-white
*/
final val WHITE: AnsiValue.B = B(scala.Console.WHITE)
- }
case class S(value: String) extends AnsiValue
- object S {
+ object S:
private[S] def apply(value: String): AnsiValue.S = new S(value)
@@ -276,31 +293,25 @@ object AnsiValue extends AnsiValueInstances with AnsiValueSyntax {
* @group style-control
*/
final val INVISIBLE: AnsiValue.S = S(scala.Console.INVISIBLE)
- }
-}
-private[toolkit] sealed trait AnsiValueInstances {
+end AnsiValue
+
+private[toolkit] sealed transparent trait AnsiValueInstances:
- implicit val monoid: Monoid[AnsiValue] = new Monoid[AnsiValue] {
+ given Monoid[AnsiValue] = new Monoid[AnsiValue]:
override def empty: AnsiValue = AnsiValue.empty
override def combine(x: AnsiValue, y: AnsiValue): AnsiValue = x.withValue(y)
- }
- implicit val show: Show[AnsiValue] = Show.fromToString
-}
-private[toolkit] sealed trait AnsiValueSyntax {
+ given Show[AnsiValue] = Show.fromToString
- implicit class AnsiTextOps(t: AnsiText) {
-
- def print[F[_]: Console]: F[Unit] = Console[F].print(t)
+private[toolkit] sealed transparent trait AnsiValueSyntax:
+ extension (t: AnsiText)
+ def print[F[_]: Console]: F[Unit] = Console[F].print(t)
def println[F[_]: Console]: F[Unit] = Console[F].println(t)
-
- def error[F[_]: Console]: F[Unit] = Console[F].error(t)
-
+ def error[F[_]: Console]: F[Unit] = Console[F].error(t)
def errorln[F[_]: Console]: F[Unit] = Console[F].errorln(t)
- }
- implicit class AnyShowableOps[T](t: T)(implicit s: Show[T] = Show.fromToString[T]) {
+ extension [T](t: T)(using show: Show[T] = Show.fromToString[T])
def ansiValue(value: AnsiValue): AnsiText = value(t)
@@ -325,5 +336,3 @@ private[toolkit] sealed trait AnsiValueSyntax {
def ansiBlink: AnsiText = ansiStyle(_.BLINK)
def ansiReversed: AnsiText = ansiStyle(_.REVERSED)
def ansiInvisible: AnsiText = ansiStyle(_.INVISIBLE)
- }
-}
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/error/ErrorLifter.scala b/core/src/main/scala/com/geirolz/app/toolkit/error/ErrorLifter.scala
index 3677403..7ca241b 100644
--- a/core/src/main/scala/com/geirolz/app/toolkit/error/ErrorLifter.scala
+++ b/core/src/main/scala/com/geirolz/app/toolkit/error/ErrorLifter.scala
@@ -4,24 +4,21 @@ import cats.{effect, Applicative}
import cats.effect.Resource
import com.geirolz.app.toolkit.\/
-trait ErrorLifter[F[_], E] { self =>
-
+trait ErrorLifter[F[_], E]:
def lift[A](f: F[A]): F[E \/ A]
-
def liftResourceFunction[U, A](f: U => Resource[F, A]): U => Resource[F, E \/ A]
+ final def liftFunction[U, A](f: U => F[A]): U => F[E \/ A] = f.andThen(lift)
- final def liftFunction[U, A](f: U => F[A]): U => F[E \/ A] = f.andThen(lift(_))
-}
-object ErrorLifter {
+object ErrorLifter:
type Resource[F[_], E] = ErrorLifter[cats.effect.Resource[F, *], E]
- implicit def toRight[F[_]: Applicative, E]: ErrorLifter[F, E] = new ErrorLifter[F, E] {
- override def lift[A](fa: F[A]): F[E \/ A] = Applicative[F].map(fa)(Right(_))
+ given toRight[F[_]: Applicative, E]: ErrorLifter[F, E] =
+ new ErrorLifter[F, E]:
+ override def lift[A](fa: F[A]): F[E \/ A] =
+ Applicative[F].map(fa)(Right(_))
- override def liftResourceFunction[U, A](
- f: U => effect.Resource[F, A]
- ): U => effect.Resource[F, E \/ A] =
- f.andThen(_.evalMap(a => lift(Applicative[F].pure(a))))
- }
-}
+ override def liftResourceFunction[U, A](
+ f: U => effect.Resource[F, A]
+ ): U => effect.Resource[F, E \/ A] =
+ f.andThen(_.evalMap(a => lift(Applicative[F].pure(a))))
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/error/MultiError.scala b/core/src/main/scala/com/geirolz/app/toolkit/error/MultiError.scala
index 393cdfd..421748b 100644
--- a/core/src/main/scala/com/geirolz/app/toolkit/error/MultiError.scala
+++ b/core/src/main/scala/com/geirolz/app/toolkit/error/MultiError.scala
@@ -3,7 +3,7 @@ package com.geirolz.app.toolkit.error
import cats.data.NonEmptyList
import cats.kernel.Semigroup
-trait MultiError[E] {
+trait MultiError[E]:
type Self <: MultiError[E]
@@ -19,15 +19,13 @@ trait MultiError[E] {
copyWith(errors.appendList(me.errors.toList))
protected def copyWith(errors: NonEmptyList[E]): Self
-}
-object MultiError {
+
+object MultiError:
def semigroup[E, ME <: E & MultiError[E]](f: NonEmptyList[E] => ME): Semigroup[E] =
(x: E, y: E) =>
- (x, y) match {
+ (x, y) match
case (m1: MultiError[?], m2: MultiError[?]) =>
(m1.asInstanceOf[MultiError[E]] + m2.asInstanceOf[MultiError[E]]).asInstanceOf[E]
case (m1: MultiError[?], e2) => m1.asInstanceOf[MultiError[E]].append(e2).asInstanceOf[E]
case (e1, e2) => f(NonEmptyList.of(e1, e2))
- }
-}
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/error/MultiException.scala b/core/src/main/scala/com/geirolz/app/toolkit/error/MultiException.scala
index c52f33f..5f57ecc 100644
--- a/core/src/main/scala/com/geirolz/app/toolkit/error/MultiException.scala
+++ b/core/src/main/scala/com/geirolz/app/toolkit/error/MultiException.scala
@@ -10,7 +10,7 @@ import scala.util.control.NoStackTrace
final class MultiException(override val errors: NonEmptyList[Throwable])
extends Throwable(MultiException.buildThrowMessage(errors))
with NoStackTrace
- with MultiError[Throwable] {
+ with MultiError[Throwable]:
override type Self = MultiException
@@ -23,13 +23,12 @@ final class MultiException(override val errors: NonEmptyList[Throwable])
override def printStackTrace(s: PrintStream): Unit =
printStackTrace(new PrintWriter(s))
- override def printStackTrace(s: PrintWriter): Unit = {
+ override def printStackTrace(s: PrintWriter): Unit =
errors.toList.foreach(e => {
e.printStackTrace(s)
s.print(s"\n${(0 to 70).map(_ => "#").mkString("")}\n")
})
s.close()
- }
override protected def copyWith(errors: NonEmptyList[Throwable]): MultiException =
new MultiException(errors)
@@ -42,20 +41,18 @@ final class MultiException(override val errors: NonEmptyList[Throwable])
@Deprecated
override def setStackTrace(stackTrace: Array[StackTraceElement]): Unit =
throw new UnsupportedOperationException
-}
-object MultiException {
+object MultiException:
- private def buildThrowMessage(errors: NonEmptyList[Throwable]): String = {
+ private def buildThrowMessage(errors: NonEmptyList[Throwable]): String =
s"""
|Multiple [${errors.size}] exceptions.
|${errors.toList
.map(ex => s" -${ex.getMessage} [${ex.getStackTrace.headOption.map(_.toString).getOrElse("")}]")
.mkString("\n")}""".stripMargin
- }
def fromFoldable[F[_]: Foldable](errors: F[Throwable]): Option[MultiException] =
- NonEmptyList.fromFoldable(errors).map(fromNel(_))
+ NonEmptyList.fromFoldable(errors).map(fromNel)
def fromNel(errors: NonEmptyList[Throwable]): MultiException =
new MultiException(errors)
@@ -63,8 +60,7 @@ object MultiException {
def of(e1: Throwable, eN: Throwable*): MultiException =
MultiException.fromNel(NonEmptyList.of(e1, eN*))
- implicit val semigroup: Semigroup[MultiException] =
+ given Semigroup[MultiException] =
(x: MultiException, y: MultiException) => x + y
- implicit val show: Show[MultiException] = Show.fromToString
-}
+ given Show[MultiException] = Show.fromToString
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/error/implicits.scala b/core/src/main/scala/com/geirolz/app/toolkit/error/implicits.scala
new file mode 100644
index 0000000..4caa0fa
--- /dev/null
+++ b/core/src/main/scala/com/geirolz/app/toolkit/error/implicits.scala
@@ -0,0 +1,18 @@
+package com.geirolz.app.toolkit.error
+
+import cats.kernel.Semigroup
+
+extension (ctx: StringContext)
+ inline def error(args: Any*): RuntimeException =
+ new RuntimeException(ctx.s(args*))
+
+extension (str: String)
+ inline def asError: RuntimeException =
+ new RuntimeException(str)
+
+given Semigroup[Throwable] = (x: Throwable, y: Throwable) =>
+ (x, y) match
+ case (m1: MultiException, m2: MultiException) => m1 + m2
+ case (e1: Throwable, m2: MultiException) => m2.prepend(e1)
+ case (m1: MultiException, e2: Throwable) => m1.append(e2)
+ case (e1: Throwable, e2: Throwable) => MultiException.of(e1, e2)
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/error/package.scala b/core/src/main/scala/com/geirolz/app/toolkit/error/package.scala
deleted file mode 100644
index d26d1de..0000000
--- a/core/src/main/scala/com/geirolz/app/toolkit/error/package.scala
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.geirolz.app.toolkit
-
-import cats.kernel.Semigroup
-
-package object error {
-
- implicit class RuntimeExpressionStringCtx(ctx: StringContext) {
- def ex(args: Any*): RuntimeException =
- new RuntimeException(ctx.s(args*)).dropFirstStackTraceElement
- }
-
- implicit class ThrowableSyntax[T <: Throwable](ex: T) {
- def dropFirstStackTraceElement: T = {
- val stackTrace = ex.getStackTrace
- if (stackTrace != null && stackTrace.length > 1)
- ex.setStackTrace(stackTrace.tail)
-
- ex
- }
- }
- implicit val throwableSemigroup: Semigroup[Throwable] = (x: Throwable, y: Throwable) =>
- (x, y) match {
- case (m1: MultiException, m2: MultiException) => m1 + m2
- case (e1: Throwable, m2: MultiException) => m2.prepend(e1)
- case (m1: MultiException, e2: Throwable) => m1.append(e2)
- case (e1: Throwable, e2: Throwable) => MultiException.of(e1, e2)
- }
-}
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/failure/FailureHandler.scala b/core/src/main/scala/com/geirolz/app/toolkit/failure/FailureHandler.scala
new file mode 100644
index 0000000..59a8092
--- /dev/null
+++ b/core/src/main/scala/com/geirolz/app/toolkit/failure/FailureHandler.scala
@@ -0,0 +1,85 @@
+package com.geirolz.app.toolkit.failure
+
+import cats.data.NonEmptyList
+import cats.syntax.all.*
+import cats.{~>, Applicative, Functor, Monad, Show}
+import com.geirolz.app.toolkit.{\/, AppMessages}
+import com.geirolz.app.toolkit.failure.FailureHandler.OnFailureBehaviour
+import com.geirolz.app.toolkit.logger.Logger
+
+case class FailureHandler[F[_], FAILURE](
+ onFailureF: FAILURE => F[OnFailureBehaviour],
+ handleFailureWithF: FAILURE => F[FAILURE \/ Unit]
+):
+ $this =>
+
+ inline def onFailure(f: FAILURE => F[OnFailureBehaviour]): FailureHandler[F, FAILURE] =
+ copy(onFailureF = f)
+
+ inline def handleFailureWith(f: FAILURE => F[FAILURE \/ Unit]): FailureHandler[F, FAILURE] =
+ copy(handleFailureWithF = f)
+
+ def mapK[G[_]](f: F ~> G): FailureHandler[G, FAILURE] =
+ FailureHandler[G, FAILURE](
+ onFailureF = (e: FAILURE) => f($this.onFailureF(e)),
+ handleFailureWithF = (e: FAILURE) => f($this.handleFailureWithF(e))
+ )
+
+ def widen[EE <: FAILURE]: FailureHandler[F, EE] =
+ this.asInstanceOf[FailureHandler[F, EE]]
+
+ def widenNel[EE](using FAILURE =:= NonEmptyList[EE]): FailureHandler[F, NonEmptyList[EE]] =
+ this.asInstanceOf[FailureHandler[F, NonEmptyList[EE]]]
+
+object FailureHandler extends FailureHandlerSyntax:
+
+ inline def apply[F[_], E](using ev: FailureHandler[F, E]): FailureHandler[F, E] = ev
+
+ def logAndCancelAll[F[_]: Monad, FAILURE](appMessages: AppMessages, logger: Logger[F]): FailureHandler[F, FAILURE] =
+ doNothing[F, FAILURE]().onFailure(failure => logger.failure(s"${appMessages.appAFailureOccurred} $failure").as(OnFailureBehaviour.CancelAll))
+
+ def cancelAll[F[_]: Applicative, FAILURE]: FailureHandler[F, FAILURE] =
+ doNothing[F, FAILURE]().onFailure(_ => OnFailureBehaviour.CancelAll.pure[F])
+
+ def doNothing[F[_]: Applicative, FAILURE](): FailureHandler[F, FAILURE] =
+ FailureHandler[F, FAILURE](
+ onFailureF = (_: FAILURE) => Applicative[F].pure(OnFailureBehaviour.DoNothing),
+ handleFailureWithF = (e: FAILURE) => Applicative[F].pure(Left(e))
+ )
+
+ sealed trait OnFailureBehaviour
+ object OnFailureBehaviour:
+ case object CancelAll extends OnFailureBehaviour
+ case object DoNothing extends OnFailureBehaviour
+
+sealed transparent trait FailureHandlerSyntax:
+
+ import cats.syntax.all.*
+
+ extension [F[+_], FAILURE](fh: FailureHandler[F, FAILURE])
+ def liftNonEmptyList(using Applicative[F]): FailureHandler[F, NonEmptyList[FAILURE]] =
+ FailureHandler[F, NonEmptyList[FAILURE]](
+ onFailureF = (failures: NonEmptyList[FAILURE]) =>
+ failures
+ .traverse(fh.onFailureF(_))
+ .map(
+ _.collectFirst { case OnFailureBehaviour.CancelAll =>
+ OnFailureBehaviour.CancelAll
+ }.getOrElse(OnFailureBehaviour.DoNothing)
+ ),
+ handleFailureWithF = (failures: NonEmptyList[FAILURE]) =>
+ failures.toList
+ .traverse(fh.handleFailureWithF(_))
+ .map(_.partitionEither(identity)._1.toNel)
+ .map {
+ case None => ().asRight[NonEmptyList[FAILURE]]
+ case Some(nelE) => nelE.asLeft[Unit]
+ }
+ )
+
+ extension [F[+_], FAILURE](fh: FailureHandler[F, NonEmptyList[FAILURE]])
+ def single(using Functor[F]): FailureHandler[F, FAILURE] =
+ FailureHandler[F, FAILURE](
+ onFailureF = (e: FAILURE) => fh.onFailureF(NonEmptyList.one(e)),
+ handleFailureWithF = (e: FAILURE) => fh.handleFailureWithF(NonEmptyList.one(e)).map(_.leftMap(_.head))
+ )
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/logger/ConsoleLogger.scala b/core/src/main/scala/com/geirolz/app/toolkit/logger/ConsoleLogger.scala
new file mode 100644
index 0000000..0694a7c
--- /dev/null
+++ b/core/src/main/scala/com/geirolz/app/toolkit/logger/ConsoleLogger.scala
@@ -0,0 +1,67 @@
+package com.geirolz.app.toolkit.logger
+
+import cats.effect.kernel.Async
+import com.geirolz.app.toolkit.SimpleAppInfo
+import com.geirolz.app.toolkit.console.AnsiValue
+import com.geirolz.app.toolkit.console.AnsiValue.AnsiText
+import com.geirolz.app.toolkit.logger.Logger.Level
+
+import java.io.PrintStream
+import cats.syntax.all.given
+
+sealed trait ConsoleLogger[F[_]] extends Logger[F]
+object ConsoleLogger:
+
+ final val defaultColorMapping: Level => AnsiValue.F =
+ case Level.Error => AnsiValue.F.RED
+ case Level.Failure => AnsiValue.F.BRIGHT_RED
+ case Level.Warn => AnsiValue.F.YELLOW
+ case Level.Info => AnsiValue.F.WHITE
+ case Level.Debug => AnsiValue.F.MAGENTA
+ case Level.Trace => AnsiValue.F.CYAN
+
+ final val defaultMsgFormatter: (SimpleAppInfo[?], Level, String) => String =
+ (info, level, message) => s"[${info.name.toString.toLowerCase}] $level - $message"
+
+ def apply[F[_]: Async](
+ appInfo: SimpleAppInfo[?],
+ minLevel: Level,
+ errorPrintStream: PrintStream = System.err,
+ outPrintStream: PrintStream = System.out,
+ colorMapping: Level => AnsiValue.F = defaultColorMapping,
+ msgFormatter: (SimpleAppInfo[?], Level, String) => String = defaultMsgFormatter
+ ): ConsoleLogger[F] =
+ new ConsoleLogger[F]:
+ override def error(message: => String): F[Unit] = log(Level.Error, message)
+ override def error(ex: Throwable)(message: => String): F[Unit] = log(Level.Error, message, Some(ex))
+ override def failure(message: => String): F[Unit] = log(Level.Failure, message)
+ override def failure(ex: Throwable)(message: => String): F[Unit] = log(Level.Failure, message, Some(ex))
+ override def warn(message: => String): F[Unit] = log(Level.Warn, message)
+ override def warn(ex: Throwable)(message: => String): F[Unit] = log(Level.Warn, message, Some(ex))
+ override def info(message: => String): F[Unit] = log(Level.Info, message)
+ override def info(ex: Throwable)(message: => String): F[Unit] = log(Level.Info, message, Some(ex))
+ override def debug(message: => String): F[Unit] = log(Level.Debug, message)
+ override def debug(ex: Throwable)(message: => String): F[Unit] = log(Level.Debug, message, Some(ex))
+ override def trace(message: => String): F[Unit] = log(Level.Trace, message)
+ override def trace(ex: Throwable)(message: => String): F[Unit] = log(Level.Trace, message, Some(ex))
+
+ private def log(level: Level, message: => String, ex: Option[Throwable] = None): F[Unit] =
+ Async[F].whenA(level >= minLevel) {
+
+ val ps: PrintStream =
+ level match
+ case Level.Error | Level.Failure => errorPrintStream
+ case _ => outPrintStream
+
+ val formattedMsg: AnsiText =
+ colorMapping(level)(msgFormatter(appInfo, level, message))
+
+ Async[F].delay(ps.println(formattedMsg)).flatMap { _ =>
+ ex match
+ case Some(e) =>
+ Async[F].delay {
+ e.printStackTrace(ps)
+ }
+ case None => Async[F].unit
+ }
+ }
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/logger/Logger.scala b/core/src/main/scala/com/geirolz/app/toolkit/logger/Logger.scala
new file mode 100644
index 0000000..a0c76ff
--- /dev/null
+++ b/core/src/main/scala/com/geirolz/app/toolkit/logger/Logger.scala
@@ -0,0 +1,71 @@
+package com.geirolz.app.toolkit.logger
+
+import cats.kernel.Order
+import cats.{~>, Show}
+
+trait Logger[F[_]]:
+ def error(message: => String): F[Unit]
+ def error(ex: Throwable)(message: => String): F[Unit]
+ def failure(message: => String): F[Unit]
+ def failure(ex: Throwable)(message: => String): F[Unit]
+ def warn(message: => String): F[Unit]
+ def warn(ex: Throwable)(message: => String): F[Unit]
+ def info(message: => String): F[Unit]
+ def info(ex: Throwable)(message: => String): F[Unit]
+ def debug(message: => String): F[Unit]
+ def debug(ex: Throwable)(message: => String): F[Unit]
+ def trace(message: => String): F[Unit]
+ def trace(ex: Throwable)(message: => String): F[Unit]
+ def mapK[G[_]](nat: F ~> G): Logger[G] = Logger.mapK(this)(nat)
+
+object Logger:
+
+ import cats.implicits.*
+
+ export NoopLogger.apply as noop
+ export ConsoleLogger.apply as console
+
+ sealed trait Level:
+ def index: Int =
+ this match
+ case Level.Error => 5
+ case Level.Failure => 4
+ case Level.Warn => 3
+ case Level.Info => 2
+ case Level.Debug => 1
+ case Level.Trace => 0
+
+ override def toString: String =
+ this match
+ case Level.Error => "ERROR"
+ case Level.Failure => "FAILURE"
+ case Level.Warn => "WARN"
+ case Level.Info => "INFO"
+ case Level.Debug => "DEBUG"
+ case Level.Trace => "TRACE"
+
+ object Level:
+ case object Error extends Level
+ case object Failure extends Level
+ case object Warn extends Level
+ case object Info extends Level
+ case object Debug extends Level
+ case object Trace extends Level
+
+ given Show[Level] = Show.fromToString
+ given Order[Level] = Order.by(_.index)
+
+ def mapK[F[_], G[_]](i: Logger[F])(nat: F ~> G): Logger[G] =
+ new Logger[G]:
+ override def error(message: => String): G[Unit] = nat(i.error(message))
+ override def error(ex: Throwable)(message: => String): G[Unit] = nat(i.error(ex)(message))
+ override def failure(message: => String): G[Unit] = nat(i.failure(message))
+ override def failure(ex: Throwable)(message: => String): G[Unit] = nat(i.failure(ex)(message))
+ override def warn(message: => String): G[Unit] = nat(i.warn(message))
+ override def warn(ex: Throwable)(message: => String): G[Unit] = nat(i.warn(ex)(message))
+ override def info(message: => String): G[Unit] = nat(i.info(message))
+ override def info(ex: Throwable)(message: => String): G[Unit] = nat(i.info(ex)(message))
+ override def debug(message: => String): G[Unit] = nat(i.debug(message))
+ override def debug(ex: Throwable)(message: => String): G[Unit] = nat(i.debug(ex)(message))
+ override def trace(message: => String): G[Unit] = nat(i.trace(message))
+ override def trace(ex: Throwable)(message: => String): G[Unit] = nat(i.trace(ex)(message))
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/logger/LoggerAdapter.scala b/core/src/main/scala/com/geirolz/app/toolkit/logger/LoggerAdapter.scala
index 6423f47..635c12e 100644
--- a/core/src/main/scala/com/geirolz/app/toolkit/logger/LoggerAdapter.scala
+++ b/core/src/main/scala/com/geirolz/app/toolkit/logger/LoggerAdapter.scala
@@ -1,24 +1,25 @@
package com.geirolz.app.toolkit.logger
-trait LoggerAdapter[LOGGER[_[_]]] {
- def toToolkit[F[_]](appLogger: LOGGER[F]): ToolkitLogger[F]
-}
-object LoggerAdapter {
- def apply[LOGGER[_[_]]: LoggerAdapter]: LoggerAdapter[LOGGER] = implicitly[LoggerAdapter[LOGGER]]
+trait LoggerAdapter[LOGGER[_[_]]]:
+ def toToolkit[F[_]](appLogger: LOGGER[F]): Logger[F]
- implicit def id[L[K[_]] <: ToolkitLogger[K]]: LoggerAdapter[L] =
- new LoggerAdapter[L] {
- override def toToolkit[F[_]](u: L[F]): ToolkitLogger[F] = new ToolkitLogger[F] {
- override def error(message: => String): F[Unit] = u.error(message)
- override def error(ex: Throwable)(message: => String): F[Unit] = u.error(ex)(message)
- override def warn(message: => String): F[Unit] = u.warn(message)
- override def warn(ex: Throwable)(message: => String): F[Unit] = u.warn(ex)(message)
- override def info(message: => String): F[Unit] = u.info(message)
- override def info(ex: Throwable)(message: => String): F[Unit] = u.info(ex)(message)
- override def debug(message: => String): F[Unit] = u.debug(message)
- override def debug(ex: Throwable)(message: => String): F[Unit] = u.debug(ex)(message)
- override def trace(message: => String): F[Unit] = u.trace(message)
- override def trace(ex: Throwable)(message: => String): F[Unit] = u.trace(ex)(message)
- }
- }
-}
+object LoggerAdapter:
+ inline def apply[LOGGER[_[_]]: LoggerAdapter]: LoggerAdapter[LOGGER] =
+ summon[LoggerAdapter[LOGGER]]
+
+ given [L[K[_]] <: Logger[K]]: LoggerAdapter[L] =
+ new LoggerAdapter[L]:
+ override def toToolkit[F[_]](u: L[F]): Logger[F] =
+ new Logger[F]:
+ override def error(message: => String): F[Unit] = u.error(message)
+ override def error(ex: Throwable)(message: => String): F[Unit] = u.error(ex)(message)
+ override def failure(message: => String): F[Unit] = u.error(message)
+ override def failure(ex: Throwable)(message: => String): F[Unit] = u.error(ex)(message)
+ override def warn(message: => String): F[Unit] = u.warn(message)
+ override def warn(ex: Throwable)(message: => String): F[Unit] = u.warn(ex)(message)
+ override def info(message: => String): F[Unit] = u.info(message)
+ override def info(ex: Throwable)(message: => String): F[Unit] = u.info(ex)(message)
+ override def debug(message: => String): F[Unit] = u.debug(message)
+ override def debug(ex: Throwable)(message: => String): F[Unit] = u.debug(ex)(message)
+ override def trace(message: => String): F[Unit] = u.trace(message)
+ override def trace(ex: Throwable)(message: => String): F[Unit] = u.trace(ex)(message)
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/logger/NoopLogger.scala b/core/src/main/scala/com/geirolz/app/toolkit/logger/NoopLogger.scala
index 14de1f1..6e2fef8 100644
--- a/core/src/main/scala/com/geirolz/app/toolkit/logger/NoopLogger.scala
+++ b/core/src/main/scala/com/geirolz/app/toolkit/logger/NoopLogger.scala
@@ -2,18 +2,19 @@ package com.geirolz.app.toolkit.logger
import cats.Applicative
-sealed trait NoopLogger[F[_]] extends ToolkitLogger[F]
-object NoopLogger {
- def apply[F[_]: Applicative]: NoopLogger[F] = new NoopLogger[F] {
- override def error(message: => String): F[Unit] = Applicative[F].unit
- override def error(ex: Throwable)(message: => String): F[Unit] = Applicative[F].unit
- override def warn(message: => String): F[Unit] = Applicative[F].unit
- override def warn(ex: Throwable)(message: => String): F[Unit] = Applicative[F].unit
- override def info(message: => String): F[Unit] = Applicative[F].unit
- override def info(ex: Throwable)(message: => String): F[Unit] = Applicative[F].unit
- override def debug(message: => String): F[Unit] = Applicative[F].unit
- override def debug(ex: Throwable)(message: => String): F[Unit] = Applicative[F].unit
- override def trace(message: => String): F[Unit] = Applicative[F].unit
- override def trace(ex: Throwable)(message: => String): F[Unit] = Applicative[F].unit
- }
-}
+sealed trait NoopLogger[F[_]] extends Logger[F]
+object NoopLogger:
+ def apply[F[_]: Applicative]: NoopLogger[F] =
+ new NoopLogger[F]:
+ override def error(message: => String): F[Unit] = Applicative[F].unit
+ override def error(ex: Throwable)(message: => String): F[Unit] = Applicative[F].unit
+ override def failure(message: => String): F[Unit] = Applicative[F].unit
+ override def failure(ex: Throwable)(message: => String): F[Unit] = Applicative[F].unit
+ override def warn(message: => String): F[Unit] = Applicative[F].unit
+ override def warn(ex: Throwable)(message: => String): F[Unit] = Applicative[F].unit
+ override def info(message: => String): F[Unit] = Applicative[F].unit
+ override def info(ex: Throwable)(message: => String): F[Unit] = Applicative[F].unit
+ override def debug(message: => String): F[Unit] = Applicative[F].unit
+ override def debug(ex: Throwable)(message: => String): F[Unit] = Applicative[F].unit
+ override def trace(message: => String): F[Unit] = Applicative[F].unit
+ override def trace(ex: Throwable)(message: => String): F[Unit] = Applicative[F].unit
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/logger/ToolkitLogger.scala b/core/src/main/scala/com/geirolz/app/toolkit/logger/ToolkitLogger.scala
deleted file mode 100644
index 3438062..0000000
--- a/core/src/main/scala/com/geirolz/app/toolkit/logger/ToolkitLogger.scala
+++ /dev/null
@@ -1,110 +0,0 @@
-package com.geirolz.app.toolkit.logger
-
-import cats.effect.kernel.Async
-import cats.kernel.Order
-import cats.{~>, Show}
-import com.geirolz.app.toolkit.SimpleAppInfo
-import com.geirolz.app.toolkit.console.AnsiValue
-import com.geirolz.app.toolkit.console.AnsiValue.AnsiText
-
-import java.io.PrintStream
-
-trait ToolkitLogger[F[_]] {
- def error(message: => String): F[Unit]
- def error(ex: Throwable)(message: => String): F[Unit]
- def warn(message: => String): F[Unit]
- def warn(ex: Throwable)(message: => String): F[Unit]
- def info(message: => String): F[Unit]
- def info(ex: Throwable)(message: => String): F[Unit]
- def debug(message: => String): F[Unit]
- def debug(ex: Throwable)(message: => String): F[Unit]
- def trace(message: => String): F[Unit]
- def trace(ex: Throwable)(message: => String): F[Unit]
- def mapK[G[_]](nat: F ~> G): ToolkitLogger[G] = ToolkitLogger.mapK(this)(nat)
-}
-object ToolkitLogger {
-
- import cats.implicits.*
-
- sealed trait Level {
-
- def index: Int = this match {
- case Level.Error => 4
- case Level.Warn => 3
- case Level.Info => 2
- case Level.Debug => 1
- case Level.Trace => 0
- }
-
- override def toString: String = this match {
- case Level.Error => "ERROR"
- case Level.Warn => "WARN"
- case Level.Info => "INFO"
- case Level.Debug => "DEBUG"
- case Level.Trace => "Trace"
- }
- }
- object Level {
- case object Error extends Level
- case object Warn extends Level
- case object Info extends Level
- case object Debug extends Level
- case object Trace extends Level
-
- implicit val show: Show[Level] = Show.fromToString
- implicit val order: Order[Level] = Order.by(_.index)
- }
-
- def console[F[_]: Async](appInfo: SimpleAppInfo[?], minLevel: Level = Level.Warn): ToolkitLogger[F] = new ToolkitLogger[F] {
- override def error(message: => String): F[Unit] = log(Level.Error, message)
- override def error(ex: Throwable)(message: => String): F[Unit] = log(Level.Error, message, Some(ex))
- override def warn(message: => String): F[Unit] = log(Level.Warn, message)
- override def warn(ex: Throwable)(message: => String): F[Unit] = log(Level.Warn, message, Some(ex))
- override def info(message: => String): F[Unit] = log(Level.Info, message)
- override def info(ex: Throwable)(message: => String): F[Unit] = log(Level.Info, message, Some(ex))
- override def debug(message: => String): F[Unit] = log(Level.Debug, message)
- override def debug(ex: Throwable)(message: => String): F[Unit] = log(Level.Debug, message, Some(ex))
- override def trace(message: => String): F[Unit] = log(Level.Trace, message)
- override def trace(ex: Throwable)(message: => String): F[Unit] = log(Level.Trace, message, Some(ex))
-
- private def log(level: Level, message: => String, ex: Option[Throwable] = None): F[Unit] =
- Async[F].whenA(level >= minLevel) {
-
- val ps: PrintStream = level match {
- case Level.Error => System.err
- case _ => System.out
- }
-
- val color: AnsiValue = level match {
- case Level.Error => AnsiValue.F.RED
- case Level.Warn => AnsiValue.F.YELLOW
- case Level.Info => AnsiValue.F.WHITE
- case Level.Debug => AnsiValue.F.MAGENTA
- case Level.Trace => AnsiValue.F.CYAN
- }
-
- val formattedMsg: AnsiText =
- color(s"[${appInfo.name.toString.toLowerCase}] $level - $message")
-
- Async[F].delay(ps.println(formattedMsg)).flatMap { _ =>
- ex match {
- case Some(e) => Async[F].delay { e.printStackTrace(ps) }
- case None => Async[F].unit
- }
- }
- }
- }
-
- def mapK[F[_], G[_]](i: ToolkitLogger[F])(nat: F ~> G): ToolkitLogger[G] = new ToolkitLogger[G] {
- override def error(message: => String): G[Unit] = nat(i.error(message))
- override def error(ex: Throwable)(message: => String): G[Unit] = nat(i.error(ex)(message))
- override def warn(message: => String): G[Unit] = nat(i.warn(message))
- override def warn(ex: Throwable)(message: => String): G[Unit] = nat(i.warn(ex)(message))
- override def info(message: => String): G[Unit] = nat(i.info(message))
- override def info(ex: Throwable)(message: => String): G[Unit] = nat(i.info(ex)(message))
- override def debug(message: => String): G[Unit] = nat(i.debug(message))
- override def debug(ex: Throwable)(message: => String): G[Unit] = nat(i.debug(ex)(message))
- override def trace(message: => String): G[Unit] = nat(i.trace(message))
- override def trace(ex: Throwable)(message: => String): G[Unit] = nat(i.trace(ex)(message))
- }
-}
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/novalues/NoConfig.scala b/core/src/main/scala/com/geirolz/app/toolkit/novalues/NoConfig.scala
index bbbed7c..058da63 100644
--- a/core/src/main/scala/com/geirolz/app/toolkit/novalues/NoConfig.scala
+++ b/core/src/main/scala/com/geirolz/app/toolkit/novalues/NoConfig.scala
@@ -3,7 +3,6 @@ package com.geirolz.app.toolkit.novalues
import cats.Show
sealed trait NoConfig
-object NoConfig {
- final val value: NoConfig = new NoConfig {}
- implicit val show: Show[NoConfig] = Show.show(_ => "[NO CONFIG]")
-}
+object NoConfig:
+ final val value: NoConfig = new NoConfig {}
+ given Show[NoConfig] = Show.show(_ => "[NO CONFIG]")
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/novalues/NoDependencies.scala b/core/src/main/scala/com/geirolz/app/toolkit/novalues/NoDependencies.scala
index 93f6f46..80e5100 100644
--- a/core/src/main/scala/com/geirolz/app/toolkit/novalues/NoDependencies.scala
+++ b/core/src/main/scala/com/geirolz/app/toolkit/novalues/NoDependencies.scala
@@ -1,6 +1,5 @@
package com.geirolz.app.toolkit.novalues
sealed trait NoDependencies
-object NoDependencies {
- final val value: NoDependencies = new NoDependencies {}
-}
+object NoDependencies:
+ private[toolkit] final val value: NoDependencies = new NoDependencies {}
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/novalues/NoFailure.scala b/core/src/main/scala/com/geirolz/app/toolkit/novalues/NoFailure.scala
new file mode 100644
index 0000000..077e594
--- /dev/null
+++ b/core/src/main/scala/com/geirolz/app/toolkit/novalues/NoFailure.scala
@@ -0,0 +1,7 @@
+package com.geirolz.app.toolkit.novalues
+
+import com.geirolz.app.toolkit.utils.=:!=
+
+sealed trait NoFailure
+object NoFailure:
+ type NotNoFailure[T] = T =:!= NoFailure
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/novalues/NoResources.scala b/core/src/main/scala/com/geirolz/app/toolkit/novalues/NoResources.scala
index 40c740c..c9d2d12 100644
--- a/core/src/main/scala/com/geirolz/app/toolkit/novalues/NoResources.scala
+++ b/core/src/main/scala/com/geirolz/app/toolkit/novalues/NoResources.scala
@@ -1,6 +1,5 @@
package com.geirolz.app.toolkit.novalues
sealed trait NoResources
-object NoResources {
+object NoResources:
final val value: NoResources = new NoResources {}
-}
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/package.scala b/core/src/main/scala/com/geirolz/app/toolkit/package.scala
deleted file mode 100644
index 336e8d3..0000000
--- a/core/src/main/scala/com/geirolz/app/toolkit/package.scala
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.geirolz.app
-
-package object toolkit {
- type \/[+A, +B] = Either[A, B]
-}
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/types.scala b/core/src/main/scala/com/geirolz/app/toolkit/types.scala
new file mode 100644
index 0000000..5ad57f9
--- /dev/null
+++ b/core/src/main/scala/com/geirolz/app/toolkit/types.scala
@@ -0,0 +1,10 @@
+package com.geirolz.app.toolkit
+
+import scala.annotation.targetName
+
+@targetName("Either")
+type \/[+A, +B] = Either[A, B]
+
+inline def ctx[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, DEPENDENCIES, RESOURCES](using
+ c: AppContext[INFO, LOGGER, CONFIG, DEPENDENCIES, RESOURCES]
+): AppContext[INFO, LOGGER, CONFIG, DEPENDENCIES, RESOURCES] = c
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/utils/=:!=.scala b/core/src/main/scala/com/geirolz/app/toolkit/utils/=:!=.scala
new file mode 100644
index 0000000..be3414b
--- /dev/null
+++ b/core/src/main/scala/com/geirolz/app/toolkit/utils/=:!=.scala
@@ -0,0 +1,17 @@
+package com.geirolz.app.toolkit.utils
+
+import scala.annotation.{implicitAmbiguous, implicitNotFound}
+import scala.language.postfixOps
+
+@implicitNotFound(msg = "Cannot prove that ${A} =:!= ${B}.")
+sealed trait =:!=[A, B]
+object =:!= {
+
+ given neq[A, B]: =:!=[A, B] = new =:!=[A, B] {}
+
+ @implicitAmbiguous(msg = "Expected a different type from ${A}")
+ given neqAmbig1[A]: =:!=[A, A] = null
+
+ @implicitAmbiguous(msg = "Expected a different type from ${A}")
+ given neqAmbig2[A]: =:!=[A, A] = null
+}
diff --git a/core/src/main/scala/com/geirolz/app/toolkit/utils/ContextFunction.scala b/core/src/main/scala/com/geirolz/app/toolkit/utils/ContextFunction.scala
new file mode 100644
index 0000000..017042c
--- /dev/null
+++ b/core/src/main/scala/com/geirolz/app/toolkit/utils/ContextFunction.scala
@@ -0,0 +1,5 @@
+package com.geirolz.app.toolkit.utils
+
+extension [A, B](f: A => B)
+ def asContextFunction: A ?=> B =
+ f(summon[A])
diff --git a/core/src/test/scala/com/geirolz/app/toolkit/AppContextAndDependenciesSuite.scala b/core/src/test/scala/com/geirolz/app/toolkit/AppContextAndDependenciesSuite.scala
new file mode 100644
index 0000000..20d47ab
--- /dev/null
+++ b/core/src/test/scala/com/geirolz/app/toolkit/AppContextAndDependenciesSuite.scala
@@ -0,0 +1,32 @@
+package com.geirolz.app.toolkit
+
+import cats.effect.{IO, Resource}
+import com.geirolz.app.toolkit.logger.Logger
+import com.geirolz.app.toolkit.testing.{TestAppInfo, TestConfig}
+
+class AppContextAndDependenciesSuite extends munit.FunSuite:
+
+ // false positive not exhaustive pattern matching ? TODO: investigate
+ test("AppContext unapply works as expected") {
+ val res = App[IO]
+ .withInfo(TestAppInfo.value)
+ .withConsoleLogger()
+ .withConfigPure(TestConfig.defaultTest)
+ .withoutResources
+ .withoutDependencies
+ .provideOne(IO.unit)
+ .run()
+ .void
+ }
+
+ // false positive not exhaustive pattern matching ? TODO: investigate
+ test("AppDependencies unapply works as expected") {
+ App[IO]
+ .withInfo(TestAppInfo.value)
+ .withConsoleLogger()
+ .withConfigPure(TestConfig.defaultTest)
+ .withoutDependencies
+ .provideOne(IO.unit)
+ .run()
+ .void
+ }
diff --git a/core/src/test/scala/com/geirolz/app/toolkit/AppResourcesAndDependenciesSuite.scala b/core/src/test/scala/com/geirolz/app/toolkit/AppResourcesAndDependenciesSuite.scala
deleted file mode 100644
index 55fd4b8..0000000
--- a/core/src/test/scala/com/geirolz/app/toolkit/AppResourcesAndDependenciesSuite.scala
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.geirolz.app.toolkit
-
-import cats.effect.{IO, Resource}
-import com.geirolz.app.toolkit.logger.ToolkitLogger
-import com.geirolz.app.toolkit.testing.{TestAppInfo, TestConfig}
-
-class AppResourcesAndDependenciesSuite extends munit.FunSuite {
-
- // false positive not exhaustive pattern matching ? TODO: investigate
- test("App.Resources unapply works as expected") {
- App[IO]
- .withInfo(TestAppInfo.value)
- .withLogger(ToolkitLogger.console[IO](_))
- .withConfig(TestConfig.defaultTest)
- .dependsOn { case _ | App.Resources(_, _, _, _, _) =>
- Resource.eval(IO.unit)
- }
- .provideOne(_ => IO.unit)
- .run_
- }
-
- // false positive not exhaustive pattern matching ? TODO: investigate
- test("App.Dependencies unapply works as expected") {
- App[IO]
- .withInfo(TestAppInfo.value)
- .withLogger(ToolkitLogger.console[IO](_))
- .withConfig(TestConfig.defaultTest)
- .withoutDependencies
- .provideOne { case _ | App.Dependencies(_, _, _, _, _, _) =>
- IO.unit
- }
- .run_
- }
-
-}
diff --git a/core/src/test/scala/com/geirolz/app/toolkit/AppSuite.scala b/core/src/test/scala/com/geirolz/app/toolkit/AppSuite.scala
index 5656eac..026ba48 100644
--- a/core/src/test/scala/com/geirolz/app/toolkit/AppSuite.scala
+++ b/core/src/test/scala/com/geirolz/app/toolkit/AppSuite.scala
@@ -2,9 +2,10 @@ package com.geirolz.app.toolkit
import cats.data.NonEmptyList
import cats.effect.{IO, Ref, Resource}
-import com.geirolz.app.toolkit.FailureHandler.OnFailureBehaviour
-import com.geirolz.app.toolkit.logger.ToolkitLogger
-import com.geirolz.app.toolkit.testing.{LabeledResource, *}
+import com.geirolz.app.toolkit.*
+import com.geirolz.app.toolkit.failure.FailureHandler.OnFailureBehaviour
+import com.geirolz.app.toolkit.novalues.NoFailure
+import com.geirolz.app.toolkit.testing.*
import scala.concurrent.duration.DurationInt
@@ -12,6 +13,55 @@ class AppSuite extends munit.CatsEffectSuite {
import EventLogger.*
import com.geirolz.app.toolkit.error.*
+ import cats.syntax.all.*
+
+ test("App releases resources once app compiled") {
+ EventLogger
+ .create[IO]
+ .flatMap(logger => {
+ implicit val loggerImplicit: EventLogger[IO] = logger
+ for {
+ counter: Ref[IO, Int] <- IO.ref(0)
+ _ <- App[IO]
+ .withInfo(TestAppInfo.value)
+ .withConsoleLogger()
+ .withConfigPure(TestConfig.defaultTest)
+ .withResources(Resource.unit.trace(LabeledResource.appResources))
+ .dependsOn(Resource.pure[IO, Ref[IO, Int]](counter).trace(LabeledResource.appDependencies))
+ .provideOne(ctx.dependencies.set(1))
+ .compile()
+ .runFullTracedApp
+
+ // assert
+ _ <- assertIO(
+ obtained = logger.events,
+ returns = List(
+ // loading resources and dependencies
+ LabeledResource.appLoader.starting,
+ LabeledResource.appResources.starting,
+ LabeledResource.appResources.succeeded,
+ LabeledResource.appResources.finalized,
+ LabeledResource.appDependencies.starting,
+ LabeledResource.appDependencies.succeeded,
+ LabeledResource.appLoader.succeeded,
+
+ // runtime
+ LabeledResource.appRuntime.starting,
+ LabeledResource.appRuntime.succeeded,
+
+ // finalizing dependencies
+ LabeledResource.appRuntime.finalized,
+ LabeledResource.appDependencies.finalized,
+ LabeledResource.appLoader.finalized
+ )
+ )
+ _ <- assertIO(
+ obtained = counter.get,
+ returns = 1
+ )
+ } yield ()
+ })
+ }
test("Loader and App work as expected with dependsOn and logic fails") {
EventLogger
@@ -22,10 +72,10 @@ class AppSuite extends munit.CatsEffectSuite {
counter: Ref[IO, Int] <- IO.ref(0)
_ <- App[IO]
.withInfo(TestAppInfo.value)
- .withLogger(ToolkitLogger.console[IO](_))
- .withConfig(TestConfig.defaultTest)
- .dependsOn(_ => Resource.pure[IO, Ref[IO, Int]](counter).trace(LabeledResource.appDependencies))
- .provideOne(_.dependencies.set(1))
+ .withConsoleLogger()
+ .withConfigPure(TestConfig.defaultTest)
+ .dependsOn(Resource.pure[IO, Ref[IO, Int]](counter).trace(LabeledResource.appDependencies))
+ .provideOne(ctx.dependencies.set(1))
.compile()
.runFullTracedApp
@@ -65,10 +115,10 @@ class AppSuite extends munit.CatsEffectSuite {
for {
_ <- App[IO]
.withInfo(TestAppInfo.value)
- .withLogger(ToolkitLogger.console[IO](_))
- .withConfig(TestConfig.defaultTest)
- .dependsOn(_ => Resource.pure[IO, Unit](()).trace(LabeledResource.appDependencies))
- .provideOneF(_ => IO.raiseError(ex"BOOM!"))
+ .withConsoleLogger()
+ .withConfigPure(TestConfig.defaultTest)
+ .dependsOn(Resource.unit[IO].trace(LabeledResource.appDependencies))
+ .provideOneF(IO.raiseError(error"BOOM!"))
.compile()
.traceAsAppLoader
.attempt
@@ -101,10 +151,10 @@ class AppSuite extends munit.CatsEffectSuite {
for {
_ <- App[IO]
.withInfo(TestAppInfo.value)
- .withLogger(ToolkitLogger.console[IO](_))
- .withConfig(TestConfig.defaultTest)
+ .withConsoleLogger()
+ .withConfigPure(TestConfig.defaultTest)
.withoutDependencies
- .provide(_ =>
+ .provideParallel(
List(
IO.sleep(300.millis),
IO.sleep(50.millis),
@@ -143,10 +193,10 @@ class AppSuite extends munit.CatsEffectSuite {
for {
_ <- App[IO]
.withInfo(TestAppInfo.value)
- .withLogger(ToolkitLogger.console[IO](_))
- .withConfig(TestConfig.defaultTest)
+ .withConsoleLogger()
+ .withConfigPure(TestConfig.defaultTest)
.withoutDependencies
- .provideOne(_ => IO.sleep(1.second))
+ .provideOne(IO.sleep(1.second))
.compile()
.runFullTracedApp
@@ -179,10 +229,10 @@ class AppSuite extends munit.CatsEffectSuite {
for {
_ <- App[IO]
.withInfo(TestAppInfo.value)
- .withLogger(ToolkitLogger.console[IO](_))
- .withConfig(TestConfig.defaultTest)
+ .withConsoleLogger()
+ .withConfigPure(TestConfig.defaultTest)
.withoutDependencies
- .provideF(_ =>
+ .provideParallelF(
IO(
List(
IO.sleep(300.millis),
@@ -225,10 +275,10 @@ class AppSuite extends munit.CatsEffectSuite {
_ <-
App[IO]
.withInfo(TestAppInfo.value)
- .withLogger(ToolkitLogger.console[IO](_))
- .withConfig(TestConfig.defaultTest)
- .dependsOn(_ => Resource.pure[IO, Unit](()).trace(LabeledResource.appDependencies))
- .provideOne(_ => IO.raiseError(ex"BOOM!"))
+ .withConsoleLogger()
+ .withConfigPure(TestConfig.defaultTest)
+ .dependsOn(Resource.pure[IO, Unit](()).trace(LabeledResource.appDependencies))
+ .provideOne(IO.raiseError(error"BOOM!"))
.compile()
.runFullTracedApp
.attempt
@@ -257,92 +307,31 @@ class AppSuite extends munit.CatsEffectSuite {
})
}
- test("beforeProviding and onFinalizeSeq with varargs work as expected") {
+ test("beforeProviding and onFinalize with List work as expected") {
EventLogger
.create[IO]
.flatMap(logger => {
- implicit val loggerImplicit: EventLogger[IO] = logger
+ given EventLogger[IO] = logger
for {
_ <- App[IO]
.withInfo(TestAppInfo.value)
- .withLogger(ToolkitLogger.console[IO](_))
- .withConfig(TestConfig.defaultTest)
+ .withConsoleLogger()
+ .withConfigPure(TestConfig.defaultTest)
.withoutDependencies
- .beforeProvidingSeq(
- _ => logger.append(Event.Custom("beforeProviding_1")),
- _ => logger.append(Event.Custom("beforeProviding_2")),
- _ => logger.append(Event.Custom("beforeProviding_3"))
- )
- .provideOne(_ => logger.append(Event.Custom("provide")))
- .onFinalizeSeq(
- _ => logger.append(Event.Custom("onFinalize_1")),
- _ => logger.append(Event.Custom("onFinalize_2")),
- _ => logger.append(Event.Custom("onFinalize_3"))
- )
- .compile()
- .runFullTracedApp
-
- // assert
- _ <- assertIO(
- obtained = logger.events,
- returns = List(
- // loading resources
- LabeledResource.appLoader.starting,
- LabeledResource.appLoader.succeeded,
-
- // runtime
- LabeledResource.appRuntime.starting,
-
- // before providing
- Event.Custom("beforeProviding_1"),
- Event.Custom("beforeProviding_2"),
- Event.Custom("beforeProviding_3"),
-
- // providing
- Event.Custom("provide"),
-
- // on finalize
- Event.Custom("onFinalize_1"),
- Event.Custom("onFinalize_2"),
- Event.Custom("onFinalize_3"),
-
- // runtime
- LabeledResource.appRuntime.succeeded,
-
- // finalizing dependencies
- LabeledResource.appRuntime.finalized,
- LabeledResource.appLoader.finalized
- )
- )
- } yield ()
- })
- }
-
- test("beforeProviding and onFinalizeSeq with List work as expected") {
- EventLogger
- .create[IO]
- .flatMap(logger => {
- implicit val loggerImplicit: EventLogger[IO] = logger
- for {
- _ <- App[IO]
- .withInfo(TestAppInfo.value)
- .withLogger(ToolkitLogger.console[IO](_))
- .withConfig(TestConfig.defaultTest)
- .withoutDependencies
- .beforeProvidingSeq(_ =>
+ .beforeProviding(
List(
logger.append(Event.Custom("beforeProviding_1")),
logger.append(Event.Custom("beforeProviding_2")),
logger.append(Event.Custom("beforeProviding_3"))
- )
+ ).sequence_
)
- .provideOne(_ => logger.append(Event.Custom("provide")))
- .onFinalizeSeq(_ =>
+ .provideOne(logger.append(Event.Custom("provide")))
+ .onFinalize(
List(
logger.append(Event.Custom("onFinalize_1")),
logger.append(Event.Custom("onFinalize_2")),
logger.append(Event.Custom("onFinalize_3"))
- )
+ ).sequence_
)
.compile()
.runFullTracedApp
@@ -389,12 +378,12 @@ class AppSuite extends munit.CatsEffectSuite {
state <- IO.ref[Boolean](false)
_ <- App[IO]
.withInfo(TestAppInfo.value)
- .withLogger(ToolkitLogger.console[IO](_))
- .withConfig(TestConfig.defaultTest)
+ .withConsoleLogger()
+ .withConfigPure(TestConfig.defaultTest)
.withoutDependencies
- .provideOne(r =>
+ .provideOne(
state.set(
- r.args.exists(
+ ctx.args.exists(
_.getVar[Int]("arg1").contains(1),
_.hasFlags("verbose", "debug")
)
@@ -418,34 +407,30 @@ class AppSuite extends munit.CatsEffectSuite {
case class Boom() extends AppError
}
- val test: IO[(Boolean, NonEmptyList[AppError] \/ Unit)] =
+ val test =
for {
state <- IO.ref[Boolean](false)
app <- App[IO, AppError]
.withInfo(TestAppInfo.value)
- .withLogger(ToolkitLogger.console[IO](_))
- .withConfig(TestConfig.defaultTest)
+ .withConsoleLogger()
+ .withConfigPure(TestConfig.defaultTest)
.withoutDependencies
- .provide(_ =>
+ .provideParallelE(
List(
IO(Left(AppError.Boom())),
IO.sleep(1.seconds) >> IO(Left(AppError.Boom())),
IO.sleep(5.seconds) >> state.set(true).as(Right(()))
)
)
- .onFailure_(res =>
- res.useTupledAll[IO[Unit]] { case (_, _, logger, _, failures) =>
- logger.error(failures.toString)
- }
- )
+ .onFailure_(failures => ctx.logger.error(failures.toString))
.runRaw()
finalState <- state.get
} yield (finalState, app)
assertIO_(
- test.map { case (state, appResult) =>
+ test.map { case (finalState, appResult) =>
assertEquals(
- obtained = state,
+ obtained = finalState,
expected = false
)
assert(cond = appResult.isLeft)
@@ -465,19 +450,17 @@ class AppSuite extends munit.CatsEffectSuite {
state <- IO.ref[Boolean](false)
app <- App[IO, AppError]
.withInfo(TestAppInfo.value)
- .withLogger(ToolkitLogger.console[IO](_))
- .withConfig(TestConfig.defaultTest)
+ .withConsoleLogger()
+ .withConfigPure(TestConfig.defaultTest)
.withoutDependencies
- .provide { _ =>
+ .provideParallelE {
List(
IO(Left(AppError.Boom())),
IO.sleep(1.seconds) >> IO(Left(AppError.Boom())),
IO.sleep(1.seconds) >> state.set(true).as(Right(()))
)
}
- .onFailure(_.useTupledAll { case (_, _, logger: ToolkitLogger[IO], _, failure: AppError) =>
- logger.error(failure.toString).as(OnFailureBehaviour.DoNothing)
- })
+ .onFailure((failure: AppError) => ctx.logger.error(failure.toString).as(OnFailureBehaviour.DoNothing))
.runRaw()
finalState <- state.get
} yield (finalState, app)
diff --git a/core/src/test/scala/com/geirolz/app/toolkit/error/ErrorSyntaxSuite.scala b/core/src/test/scala/com/geirolz/app/toolkit/error/ErrorSyntaxSuite.scala
index b8134e1..8838a85 100644
--- a/core/src/test/scala/com/geirolz/app/toolkit/error/ErrorSyntaxSuite.scala
+++ b/core/src/test/scala/com/geirolz/app/toolkit/error/ErrorSyntaxSuite.scala
@@ -1,8 +1,11 @@
package com.geirolz.app.toolkit.error
-class ErrorSyntaxSuite extends munit.FunSuite {
+class ErrorSyntaxSuite extends munit.FunSuite:
- test("MultiError") {
- ex"BOOM!".printStackTrace()
+ test("error build exception from string context") {
+ error"BOOM!".printStackTrace()
+ }
+
+ test("asError build exception from string") {
+ "BOOM!".asError.printStackTrace()
}
-}
diff --git a/core/src/test/scala/com/geirolz/app/toolkit/error/MultiExceptionSuite.scala b/core/src/test/scala/com/geirolz/app/toolkit/error/MultiExceptionSuite.scala
index 25428c9..9428add 100644
--- a/core/src/test/scala/com/geirolz/app/toolkit/error/MultiExceptionSuite.scala
+++ b/core/src/test/scala/com/geirolz/app/toolkit/error/MultiExceptionSuite.scala
@@ -2,7 +2,7 @@ package com.geirolz.app.toolkit.error
import cats.data.NonEmptyList
-class MultiExceptionSuite extends munit.FunSuite {
+class MultiExceptionSuite extends munit.FunSuite:
test("Test printStackTrace") {
val ex = MultiException.fromNel(
@@ -66,4 +66,3 @@ class MultiExceptionSuite extends munit.FunSuite {
ex.printStackTrace()
}
-}
diff --git a/core/src/test/scala/com/geirolz/app/toolkit/logger/AnsiValueSuite.scala b/core/src/test/scala/com/geirolz/app/toolkit/logger/AnsiValueSuite.scala
index d0bfc4d..5420003 100644
--- a/core/src/test/scala/com/geirolz/app/toolkit/logger/AnsiValueSuite.scala
+++ b/core/src/test/scala/com/geirolz/app/toolkit/logger/AnsiValueSuite.scala
@@ -4,7 +4,7 @@ import cats.effect.IO
import cats.effect.unsafe.IORuntime
import com.geirolz.app.toolkit.console.AnsiValue
-class AnsiValueSuite extends munit.FunSuite {
+class AnsiValueSuite extends munit.FunSuite:
import AnsiValue.*
@@ -44,10 +44,9 @@ class AnsiValueSuite extends munit.FunSuite {
}
test("Test syntax") {
- implicit val runtime: IORuntime = cats.effect.unsafe.IORuntime.global
+ given IORuntime = cats.effect.unsafe.IORuntime.global
"MY MESSAGE"
.ansi(fg = _.RED, bg = _.BLUE, s = _.UNDERLINED)
.println[IO]
.unsafeRunSync()
}
-}
diff --git a/core/src/test/scala/com/geirolz/app/toolkit/testing/TestAppInfo.scala b/core/src/test/scala/com/geirolz/app/toolkit/testing/TestAppInfo.scala
index 98c1f5e..699b6b4 100644
--- a/core/src/test/scala/com/geirolz/app/toolkit/testing/TestAppInfo.scala
+++ b/core/src/test/scala/com/geirolz/app/toolkit/testing/TestAppInfo.scala
@@ -12,15 +12,14 @@ case class TestAppInfo(
sbtVersion: String,
javaVersion: Option[String],
builtOn: LocalDateTime
-) extends SimpleAppInfo[String] {
+) extends SimpleAppInfo[String]:
override val buildRefName: String = SimpleAppInfo.genRefNameString(
name = name,
version = version,
builtOn = builtOn
)
-}
-object TestAppInfo {
+object TestAppInfo:
val value: TestAppInfo = TestAppInfo(
name = "AppTest",
description = "An app test",
@@ -30,4 +29,3 @@ object TestAppInfo {
javaVersion = None,
builtOn = LocalDateTime.now()
)
-}
diff --git a/core/src/test/scala/com/geirolz/app/toolkit/testing/TestConfig.scala b/core/src/test/scala/com/geirolz/app/toolkit/testing/TestConfig.scala
index f23b78f..d756a91 100644
--- a/core/src/test/scala/com/geirolz/app/toolkit/testing/TestConfig.scala
+++ b/core/src/test/scala/com/geirolz/app/toolkit/testing/TestConfig.scala
@@ -3,9 +3,6 @@ package com.geirolz.app.toolkit.testing
import cats.Show
case class TestConfig(value: String)
-object TestConfig {
-
+object TestConfig:
def defaultTest: TestConfig = TestConfig("test_config")
-
- implicit val show: Show[TestConfig] = Show.fromToString
-}
+ given Show[TestConfig] = Show.fromToString
diff --git a/docs/compiled/README.md b/docs/compiled/README.md
index 62204c3..572e337 100644
--- a/docs/compiled/README.md
+++ b/docs/compiled/README.md
@@ -70,31 +70,27 @@ libraryDependencies += "com.github.geirolz" %% "toolkit" % "0.0.11"
```scala
import cats.Show
import cats.effect.{Resource, IO}
-import com.geirolz.app.toolkit.{App, SimpleAppInfo}
-import com.geirolz.app.toolkit.logger.ToolkitLogger
+import com.geirolz.app.toolkit.*
+import com.geirolz.app.toolkit.logger.ConsoleLogger
import com.geirolz.app.toolkit.novalues.NoResources
// Define config
case class Config(host: String, port: Int)
-
-object Config {
- implicit val show: Show[Config] = Show.fromToString
-}
+object Config:
+ given Show[Config] = Show.fromToString
// Define service dependencies
case class AppDependencyServices(kafkaConsumer: KafkaConsumer[IO])
-object AppDependencyServices {
- def resource(res: App.Resources[SimpleAppInfo[String], ToolkitLogger[IO], Config, NoResources]): Resource[IO, AppDependencyServices] =
- Resource.pure(AppDependencyServices(KafkaConsumer.fake))
-}
+object AppDependencyServices:
+ def resource(using AppContext.NoDepsAndRes[SimpleAppInfo[String], ConsoleLogger[IO], Config]): Resource[IO, AppDependencyServices] =
+ Resource.pure(AppDependencyServices(KafkaConsumer.fake))
// A stubbed kafka consumer
-trait KafkaConsumer[F[_]] {
+trait KafkaConsumer[F[_]]:
def consumeFrom(name: String): fs2.Stream[F, KafkaConsumer.KafkaRecord]
-}
-object KafkaConsumer {
+object KafkaConsumer:
import scala.concurrent.duration.DurationInt
@@ -105,7 +101,6 @@ object KafkaConsumer {
fs2.Stream
.eval(IO.randomUUID.map(t => KafkaRecord(t.toString)).flatTap(_ => IO.sleep(5.seconds)))
.repeat
-}
```
3. **Build Your Application:** Build your application using the Toolkit DSL and execute it. Toolkit
@@ -113,35 +108,34 @@ object KafkaConsumer {
```scala
import cats.effect.{ExitCode, IO, IOApp}
-import com.geirolz.app.toolkit.{App, SimpleAppInfo}
-import com.geirolz.app.toolkit.logger.ToolkitLogger
-
-object Main extends IOApp {
- override def run(args: List[String]): IO[ExitCode] =
- App[IO]
- .withInfo(
- SimpleAppInfo.string(
- name = "toolkit",
- version = "0.0.1",
- scalaVersion = "2.13.10",
- sbtVersion = "1.8.0"
+import com.geirolz.app.toolkit.*
+import com.geirolz.app.toolkit.logger.Logger
+
+object Main extends IOApp:
+ override def run(args: List[String]): IO[ExitCode] =
+ App[IO]
+ .withInfo(
+ SimpleAppInfo.string(
+ name = "toolkit",
+ version = "0.0.1",
+ scalaVersion = "2.13.10",
+ sbtVersion = "1.8.0"
+ )
+ )
+ .withConsoleLogger()
+ .withConfigF(IO.pure(Config("localhost", 8080)))
+ .dependsOn(AppDependencyServices.resource)
+ .beforeProviding(ctx.logger.info("CUSTOM PRE-PROVIDING"))
+ .provideOne(
+ // Kafka consumer
+ ctx.dependencies.kafkaConsumer
+ .consumeFrom("test-topic")
+ .evalTap(record => ctx.logger.info(s"Received record $record"))
+ .compile
+ .drain
)
- )
- .withLogger(ToolkitLogger.console[IO](_))
- .withConfigLoader(_ => IO.pure(Config("localhost", 8080)))
- .dependsOn(AppDependencyServices.resource(_))
- .beforeProviding(_.logger.info("CUSTOM PRE-PROVIDING"))
- .provideOne(deps =>
- // Kafka consumer
- deps.dependencies.kafkaConsumer
- .consumeFrom("test-topic")
- .evalTap(record => deps.logger.info(s"Received record $record"))
- .compile
- .drain
- )
- .onFinalize(_.logger.info("CUSTOM END"))
- .run(args)
-}
+ .onFinalize(ctx.logger.info("CUSTOM END"))
+ .run(args)
```
Check a full example [here](https://github.com/geirolz/toolkit/tree/main/examples)
diff --git a/docs/compiled/integrations.md b/docs/compiled/integrations.md
index 6376cfa..79050a0 100644
--- a/docs/compiled/integrations.md
+++ b/docs/compiled/integrations.md
@@ -54,11 +54,11 @@ import com.geirolz.app.toolkit.config.pureconfig.*
case class TestConfig(value: String)
-object TestConfig {
- implicit val show: Show[TestConfig] = Show.fromToString
- implicit val configReader: pureconfig.ConfigReader[TestConfig] =
+object TestConfig:
+ given Show[TestConfig] = Show.fromToString
+ given pureconfig.ConfigReader[TestConfig] =
pureconfig.ConfigReader.forProduct1("value")(TestConfig.apply)
-}
+
App[IO]
.withInfo(
@@ -69,10 +69,11 @@ App[IO]
sbtVersion = "1.8.0"
)
)
- .withConfigLoader(pureconfigLoader[IO, TestConfig])
+ .withConfigF(pureconfigLoader[IO, TestConfig])
.withoutDependencies
- .provideOne(_ => IO.unit)
- .run_
+ .provideOne(IO.unit)
+ .run()
+ .void
```
## [Log4cats](https://github.com/typelevel/log4cats)
@@ -155,13 +156,12 @@ the whole app dependencies to provide a custom `Fly4s` instance you can use `bef
import cats.Show
import cats.effect.IO
import com.geirolz.app.toolkit.fly4s.*
-import com.geirolz.app.toolkit.{App, SimpleAppInfo}
+import com.geirolz.app.toolkit.*
case class TestConfig(dbUrl: String, dbUser: Option[String], dbPassword: Option[Array[Char]])
-object TestConfig {
- implicit val show: Show[TestConfig] = Show.fromToString
-}
+object TestConfig:
+ given Show[TestConfig] = Show.fromToString
App[IO]
.withInfo(
@@ -172,7 +172,7 @@ App[IO]
sbtVersion = "1.8.0"
)
)
- .withConfig(
+ .withConfigPure(
TestConfig(
dbUrl = "jdbc:postgresql://localhost:5432/toolkit",
dbUser = Some("postgres"),
@@ -181,12 +181,13 @@ App[IO]
)
.withoutDependencies
.beforeProviding(
- migrateDatabaseWithConfig(
- url = _.dbUrl,
- user = _.dbUser,
- password = _.dbPassword
+ migrateDatabaseWith(
+ url = ctx.config.dbUrl,
+ user = ctx.config.dbUser,
+ password = ctx.config.dbPassword
)
)
- .provideOne(_ => IO.unit)
- .run_
+ .provideOne(IO.unit)
+ .run()
+ .void
```
\ No newline at end of file
diff --git a/docs/source/README.md b/docs/source/README.md
index 5e28d44..e4d12ac 100644
--- a/docs/source/README.md
+++ b/docs/source/README.md
@@ -35,6 +35,8 @@ Please, drop a ⭐️ if you are interested in this project and you want to supp
## Features
+> Resources --build--> Dependencies --> [Finalize Resources] --build--> App Logic -> [Finalize Dependencies]
+
- **Resource Management:** Toolkit simplifies the management of application resources, such as configuration
settings, logging, and custom resources. By abstracting away the resource handling, it reduces boilerplate code and
provides a clean and concise syntax for managing resources.
@@ -47,10 +49,8 @@ Please, drop a ⭐️ if you are interested in this project and you want to supp
about, and maintain.
## Notes
-
-- All dependencies and resources are released at the end of the app execution as defined as `Resource[F, *]`.
-- If you need to release a resource before the end of the app execution you should use `Resource.use` or equivalent to
- build what you need as dependency.
+- Resources are released before providing the app execution.
+- Dependencies are released at the end of the app execution as defined as `Resource[F, *]`.
- If you need to run an infinite task using `provide*` you should use `F.never` or equivalent to keep the task running.
## Getting Started
@@ -70,31 +70,27 @@ libraryDependencies += "com.github.geirolz" %% "toolkit" % "@VERSION@"
```scala mdoc:silent
import cats.Show
import cats.effect.{Resource, IO}
-import com.geirolz.app.toolkit.{App, SimpleAppInfo}
-import com.geirolz.app.toolkit.logger.ToolkitLogger
+import com.geirolz.app.toolkit.*
+import com.geirolz.app.toolkit.logger.ConsoleLogger
import com.geirolz.app.toolkit.novalues.NoResources
// Define config
case class Config(host: String, port: Int)
-
-object Config {
- implicit val show: Show[Config] = Show.fromToString
-}
+object Config:
+ given Show[Config] = Show.fromToString
// Define service dependencies
case class AppDependencyServices(kafkaConsumer: KafkaConsumer[IO])
-object AppDependencyServices {
- def resource(res: App.Resources[SimpleAppInfo[String], ToolkitLogger[IO], Config, NoResources]): Resource[IO, AppDependencyServices] =
- Resource.pure(AppDependencyServices(KafkaConsumer.fake))
-}
+object AppDependencyServices:
+ def resource(using AppContext.NoDepsAndRes[SimpleAppInfo[String], ConsoleLogger[IO], Config]): Resource[IO, AppDependencyServices] =
+ Resource.pure(AppDependencyServices(KafkaConsumer.fake))
// A stubbed kafka consumer
-trait KafkaConsumer[F[_]] {
+trait KafkaConsumer[F[_]]:
def consumeFrom(name: String): fs2.Stream[F, KafkaConsumer.KafkaRecord]
-}
-object KafkaConsumer {
+object KafkaConsumer:
import scala.concurrent.duration.DurationInt
@@ -105,7 +101,6 @@ object KafkaConsumer {
fs2.Stream
.eval(IO.randomUUID.map(t => KafkaRecord(t.toString)).flatTap(_ => IO.sleep(5.seconds)))
.repeat
-}
```
3. **Build Your Application:** Build your application using the Toolkit DSL and execute it. Toolkit
@@ -113,35 +108,34 @@ object KafkaConsumer {
```scala mdoc:silent
import cats.effect.{ExitCode, IO, IOApp}
-import com.geirolz.app.toolkit.{App, SimpleAppInfo}
-import com.geirolz.app.toolkit.logger.ToolkitLogger
-
-object Main extends IOApp {
- override def run(args: List[String]): IO[ExitCode] =
- App[IO]
- .withInfo(
- SimpleAppInfo.string(
- name = "toolkit",
- version = "0.0.1",
- scalaVersion = "2.13.10",
- sbtVersion = "1.8.0"
+import com.geirolz.app.toolkit.*
+import com.geirolz.app.toolkit.logger.Logger
+
+object Main extends IOApp:
+ override def run(args: List[String]): IO[ExitCode] =
+ App[IO]
+ .withInfo(
+ SimpleAppInfo.string(
+ name = "toolkit",
+ version = "0.0.1",
+ scalaVersion = "2.13.10",
+ sbtVersion = "1.8.0"
+ )
+ )
+ .withConsoleLogger()
+ .withConfigF(IO.pure(Config("localhost", 8080)))
+ .dependsOn(AppDependencyServices.resource)
+ .beforeProviding(ctx.logger.info("CUSTOM PRE-PROVIDING"))
+ .provideOne(
+ // Kafka consumer
+ ctx.dependencies.kafkaConsumer
+ .consumeFrom("test-topic")
+ .evalTap(record => ctx.logger.info(s"Received record $record"))
+ .compile
+ .drain
)
- )
- .withLogger(ToolkitLogger.console[IO](_))
- .withConfigLoader(_ => IO.pure(Config("localhost", 8080)))
- .dependsOn(AppDependencyServices.resource(_))
- .beforeProviding(_.logger.info("CUSTOM PRE-PROVIDING"))
- .provideOne(deps =>
- // Kafka consumer
- deps.dependencies.kafkaConsumer
- .consumeFrom("test-topic")
- .evalTap(record => deps.logger.info(s"Received record $record"))
- .compile
- .drain
- )
- .onFinalize(_.logger.info("CUSTOM END"))
- .run(args)
-}
+ .onFinalize(ctx.logger.info("CUSTOM END"))
+ .run(args)
```
Check a full example [here](https://github.com/geirolz/toolkit/tree/main/examples)
diff --git a/docs/source/integrations.md b/docs/source/integrations.md
index f1dcfbf..ad6663a 100644
--- a/docs/source/integrations.md
+++ b/docs/source/integrations.md
@@ -54,11 +54,11 @@ import com.geirolz.app.toolkit.config.pureconfig.*
case class TestConfig(value: String)
-object TestConfig {
- implicit val show: Show[TestConfig] = Show.fromToString
- implicit val configReader: pureconfig.ConfigReader[TestConfig] =
+object TestConfig:
+ given Show[TestConfig] = Show.fromToString
+ given pureconfig.ConfigReader[TestConfig] =
pureconfig.ConfigReader.forProduct1("value")(TestConfig.apply)
-}
+
App[IO]
.withInfo(
@@ -69,10 +69,11 @@ App[IO]
sbtVersion = "1.8.0"
)
)
- .withConfigLoader(pureconfigLoader[IO, TestConfig])
+ .withConfigF(pureconfigLoader[IO, TestConfig])
.withoutDependencies
- .provideOne(_ => IO.unit)
- .run_
+ .provideOne(IO.unit)
+ .run()
+ .void
```
## [Log4cats](https://github.com/typelevel/log4cats)
@@ -155,13 +156,12 @@ the whole app dependencies to provide a custom `Fly4s` instance you can use `bef
import cats.Show
import cats.effect.IO
import com.geirolz.app.toolkit.fly4s.*
-import com.geirolz.app.toolkit.{App, SimpleAppInfo}
+import com.geirolz.app.toolkit.*
case class TestConfig(dbUrl: String, dbUser: Option[String], dbPassword: Option[Array[Char]])
-object TestConfig {
- implicit val show: Show[TestConfig] = Show.fromToString
-}
+object TestConfig:
+ given Show[TestConfig] = Show.fromToString
App[IO]
.withInfo(
@@ -172,7 +172,7 @@ App[IO]
sbtVersion = "1.8.0"
)
)
- .withConfig(
+ .withConfigPure(
TestConfig(
dbUrl = "jdbc:postgresql://localhost:5432/toolkit",
dbUser = Some("postgres"),
@@ -181,12 +181,13 @@ App[IO]
)
.withoutDependencies
.beforeProviding(
- migrateDatabaseWithConfig(
- url = _.dbUrl,
- user = _.dbUser,
- password = _.dbPassword
+ migrateDatabaseWith(
+ url = ctx.config.dbUrl,
+ user = ctx.config.dbUser,
+ password = ctx.config.dbPassword
)
)
- .provideOne(_ => IO.unit)
- .run_
+ .provideOne(IO.unit)
+ .run()
+ .void
```
\ No newline at end of file
diff --git a/examples/buildinfo.properties b/examples/buildinfo.properties
index 7e3e406..8bf017b 100644
--- a/examples/buildinfo.properties
+++ b/examples/buildinfo.properties
@@ -1,2 +1,2 @@
-#Sun Jul 09 14:35:34 CEST 2023
-buildnumber=210
+#Thu Feb 01 10:04:55 CET 2024
+buildnumber=241
diff --git a/examples/src/main/resources/document.xml b/examples/src/main/resources/document.xml
deleted file mode 100644
index ecda463..0000000
--- a/examples/src/main/resources/document.xml
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/examples/src/main/resources/host-table.txt b/examples/src/main/resources/host-table.txt
new file mode 100644
index 0000000..f48f220
--- /dev/null
+++ b/examples/src/main/resources/host-table.txt
@@ -0,0 +1 @@
+localhost:127.0.0.1
\ No newline at end of file
diff --git a/examples/src/main/scala-2/com/geirolz/example/app/AppConfig.scala b/examples/src/main/scala-2/com/geirolz/example/app/AppConfig.scala
deleted file mode 100644
index a220a8e..0000000
--- a/examples/src/main/scala-2/com/geirolz/example/app/AppConfig.scala
+++ /dev/null
@@ -1,39 +0,0 @@
-package com.geirolz.example.app
-
-import cats.Show
-import com.comcast.ip4s.{Hostname, Port}
-import com.geirolz.app.toolkit.config.Secret
-import io.circe.Encoder
-import pureconfig.ConfigReader
-
-case class AppConfig(
- httpServer: HttpServerConfig,
- kafkaBroker: KafkaBrokerSetting,
- databasePassword: Secret[String]
-)
-object AppConfig {
-
- import io.circe.generic.auto.*
- import io.circe.syntax.*
- import pureconfig.generic.auto.*
- import pureconfig.generic.semiauto.*
- import pureconfig.module.ip4s.*
-
- implicit val configReader: ConfigReader[AppConfig] = deriveReader[AppConfig]
-
- // ------------------- CIRCE -------------------
- implicit val hostnameCirceEncoder: Encoder[Hostname] =
- Encoder.encodeString.contramap(_.toString)
-
- implicit val portCirceEncoder: Encoder[Port] =
- Encoder.encodeInt.contramap(_.value)
-
- implicit def secretEncoder[T]: Encoder[Secret[T]] =
- Encoder.encodeString.contramap(_.toString)
-
- implicit val showInstanceForConfig: Show[AppConfig] = _.asJson.toString()
-}
-
-case class HttpServerConfig(port: Port, host: Hostname)
-
-case class KafkaBrokerSetting(host: Hostname)
diff --git a/examples/src/main/scala-2/com/geirolz/example/app/AppDependencyServices.scala b/examples/src/main/scala-2/com/geirolz/example/app/AppDependencyServices.scala
deleted file mode 100644
index e93aae8..0000000
--- a/examples/src/main/scala-2/com/geirolz/example/app/AppDependencyServices.scala
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.geirolz.example.app
-
-import cats.effect.{IO, Resource}
-import com.geirolz.app.toolkit.App
-import com.geirolz.app.toolkit.novalues.NoResources
-import com.geirolz.example.app.provided.KafkaConsumer
-import org.typelevel.log4cats.SelfAwareStructuredLogger
-
-case class AppDependencyServices(
- kafkaConsumer: KafkaConsumer[IO]
-)
-object AppDependencyServices {
- def resource(res: App.Resources[AppInfo, SelfAwareStructuredLogger[IO], AppConfig, NoResources]): Resource[IO, AppDependencyServices] =
- Resource.pure(
- AppDependencyServices(
- KafkaConsumer.fake(res.config.kafkaBroker.host)
- )
- )
-}
diff --git a/examples/src/main/scala-2/com/geirolz/example/app/AppInfo.scala b/examples/src/main/scala-2/com/geirolz/example/app/AppInfo.scala
deleted file mode 100644
index 3dd9483..0000000
--- a/examples/src/main/scala-2/com/geirolz/example/app/AppInfo.scala
+++ /dev/null
@@ -1,39 +0,0 @@
-package com.geirolz.example.app
-
-import cats.Show
-import com.geirolz.app.toolkit.SimpleAppInfo
-
-import java.time.{Instant, LocalDateTime, ZoneOffset}
-
-class AppInfo private (
- val name: String,
- val version: String,
- val scalaVersion: String,
- val sbtVersion: String,
- val buildRefName: String,
- val builtOn: LocalDateTime
-) extends SimpleAppInfo[String]
-object AppInfo {
-
- val fromBuildInfo: AppInfo = {
- val builtOn: LocalDateTime = LocalDateTime.ofInstant(
- Instant.ofEpochMilli(BuildInfo.builtAtMillis),
- ZoneOffset.UTC
- )
-
- new AppInfo(
- name = BuildInfo.name,
- version = BuildInfo.version,
- scalaVersion = BuildInfo.scalaVersion,
- sbtVersion = BuildInfo.sbtVersion,
- buildRefName = SimpleAppInfo.genRefNameString(
- name = BuildInfo.name,
- version = BuildInfo.version,
- builtOn = builtOn
- ),
- builtOn = builtOn
- )
- }
-
- implicit val show: Show[AppInfo] = Show.fromToString
-}
diff --git a/examples/src/main/scala-2/com/geirolz/example/app/AppMain.scala b/examples/src/main/scala-2/com/geirolz/example/app/AppMain.scala
deleted file mode 100644
index 93914a6..0000000
--- a/examples/src/main/scala-2/com/geirolz/example/app/AppMain.scala
+++ /dev/null
@@ -1,34 +0,0 @@
-package com.geirolz.example.app
-
-import cats.effect.{ExitCode, IO, IOApp}
-import com.geirolz.app.toolkit.App
-import com.geirolz.app.toolkit.config.pureconfig.pureconfigLoader
-import com.geirolz.example.app.provided.AppHttpServer
-import org.typelevel.log4cats.slf4j.Slf4jLogger
-
-object AppMain extends IOApp {
-
- override def run(args: List[String]): IO[ExitCode] =
- App[IO]
- .withInfo(AppInfo.fromBuildInfo)
- .withLogger(Slf4jLogger.getLogger[IO])
- .withConfigLoader(pureconfigLoader[IO, AppConfig])
- .dependsOn(AppDependencyServices.resource(_))
- .beforeProviding(_.logger.info("CUSTOM PRE-RUN"))
- .provide(deps =>
- List(
- // HTTP server
- AppHttpServer.resource(deps.config).useForever,
-
- // Kafka consumer
- deps.dependencies.kafkaConsumer
- .consumeFrom("test-topic")
- .evalTap(record => deps.logger.info(s"Received record $record"))
- .compile
- .drain
- .foreverM
- )
- )
- .onFinalize(_.logger.info("CUSTOM END"))
- .run(args)
-}
diff --git a/examples/src/main/scala-2/com/geirolz/example/app/AppWithFailures.scala b/examples/src/main/scala-2/com/geirolz/example/app/AppWithFailures.scala
deleted file mode 100644
index 49f4d7c..0000000
--- a/examples/src/main/scala-2/com/geirolz/example/app/AppWithFailures.scala
+++ /dev/null
@@ -1,33 +0,0 @@
-package com.geirolz.example.app
-
-import cats.effect.{ExitCode, IO, IOApp}
-import com.geirolz.app.toolkit.App
-import com.geirolz.app.toolkit.config.pureconfig.pureconfigLoader
-import com.geirolz.example.app.provided.AppHttpServer
-import org.typelevel.log4cats.slf4j.Slf4jLogger
-
-object AppWithFailures extends IOApp {
-
- override def run(args: List[String]): IO[ExitCode] =
- App[IO, AppError]
- .withInfo(AppInfo.fromBuildInfo)
- .withLogger(Slf4jLogger.getLogger[IO])
- .withConfigLoader(pureconfigLoader[IO, AppConfig])
- .dependsOn(AppDependencyServices.resource(_))
- .beforeProviding(_.logger.info("CUSTOM PRE-RUN"))
- .provide(deps =>
- List(
- // HTTP server
- AppHttpServer.resource(deps.config).useForever,
-
- // Kafka consumer
- deps.dependencies.kafkaConsumer
- .consumeFrom("test-topic")
- .evalTap(record => deps.logger.info(s"Received record $record"))
- .compile
- .drain
- )
- )
- .onFinalize(_.logger.info("CUSTOM END"))
- .run(args)
-}
diff --git a/examples/src/main/scala-2/com/geirolz/example/app/provided/AppHttpServer.scala b/examples/src/main/scala-2/com/geirolz/example/app/provided/AppHttpServer.scala
deleted file mode 100644
index edc5af1..0000000
--- a/examples/src/main/scala-2/com/geirolz/example/app/provided/AppHttpServer.scala
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.geirolz.example.app.provided
-
-import com.geirolz.example.app.AppConfig
-import org.http4s.ember.server.EmberServerBuilder
-import org.http4s.server.Server
-
-object AppHttpServer {
-
- import cats.effect.*
- import org.http4s.*
- import org.http4s.dsl.io.*
-
- def resource(config: AppConfig): Resource[IO, Server] =
- EmberServerBuilder
- .default[IO]
- .withHost(config.httpServer.host)
- .withPort(config.httpServer.port)
- .withHttpApp(
- HttpRoutes
- .of[IO] { case GET -> Root / "hello" / name =>
- Ok(s"Hello, $name.")
- }
- .orNotFound
- )
- .build
-}
diff --git a/examples/src/main/scala-2/com/geirolz/example/app/provided/KafkaConsumer.scala b/examples/src/main/scala-2/com/geirolz/example/app/provided/KafkaConsumer.scala
deleted file mode 100644
index 3954f11..0000000
--- a/examples/src/main/scala-2/com/geirolz/example/app/provided/KafkaConsumer.scala
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.geirolz.example.app.provided
-
-import cats.effect.IO
-import com.comcast.ip4s.Hostname
-import com.geirolz.example.app.provided.KafkaConsumer.KafkaRecord
-
-import scala.annotation.unused
-import scala.concurrent.duration.DurationInt
-
-trait KafkaConsumer[F[_]] {
- def consumeFrom(@unused name: String): fs2.Stream[F, KafkaRecord]
-}
-object KafkaConsumer {
-
- case class KafkaRecord(value: String)
-
- def fake(@unused host: Hostname): KafkaConsumer[IO] =
- (_: String) =>
- fs2.Stream
- .eval(IO.randomUUID.map(t => KafkaRecord(t.toString)).flatTap(_ => IO.sleep(5.seconds)))
- .repeat
-
-}
diff --git a/examples/src/main/scala-3/com/geirolz/example/app/AppDependencyServices.scala b/examples/src/main/scala-3/com/geirolz/example/app/AppDependencyServices.scala
deleted file mode 100644
index 4ae2774..0000000
--- a/examples/src/main/scala-3/com/geirolz/example/app/AppDependencyServices.scala
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.geirolz.example.app
-
-import cats.effect.{IO, Resource}
-import com.geirolz.app.toolkit.App
-import com.geirolz.app.toolkit.novalues.NoResources
-import com.geirolz.example.app.provided.KafkaConsumer
-import org.typelevel.log4cats.SelfAwareStructuredLogger
-
-case class AppDependencyServices(
- kafkaConsumer: KafkaConsumer[IO]
-)
-object AppDependencyServices:
-
- def resource(res: App.Resources[AppInfo, SelfAwareStructuredLogger[IO], AppConfig, NoResources]): Resource[IO, AppDependencyServices] =
- Resource.pure(
- AppDependencyServices(
- KafkaConsumer.fake(res.config.kafkaBroker.host)
- )
- )
diff --git a/examples/src/main/scala-3/com/geirolz/example/app/AppMain.scala b/examples/src/main/scala-3/com/geirolz/example/app/AppMain.scala
deleted file mode 100644
index 2ae716d..0000000
--- a/examples/src/main/scala-3/com/geirolz/example/app/AppMain.scala
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.geirolz.example.app
-
-import cats.effect.{ExitCode, IO, IOApp}
-import com.geirolz.app.toolkit.App
-import com.geirolz.app.toolkit.logger.log4CatsLoggerAdapter
-import com.geirolz.app.toolkit.config.pureconfig.*
-import com.geirolz.example.app.provided.AppHttpServer
-import org.typelevel.log4cats.slf4j.Slf4jLogger
-
-object AppMain extends IOApp:
- override def run(args: List[String]): IO[ExitCode] =
- App[IO]
- .withInfo(AppInfo.fromBuildInfo)
- .withLogger(Slf4jLogger.getLogger[IO])
- .withConfigLoader(pureconfigLoader[IO, AppConfig])
- .dependsOn(AppDependencyServices.resource(_))
- .beforeProviding(_.logger.info("CUSTOM PRE-RUN"))
- .provide(deps =>
- List(
- // HTTP server
- AppHttpServer.resource(deps.config).useForever,
-
- // Kafka consumer
- deps.dependencies.kafkaConsumer
- .consumeFrom("test-topic")
- .evalTap(record => deps.logger.info(s"Received record $record"))
- .compile
- .drain
- )
- )
- .onFinalize(_.logger.info("CUSTOM END"))
- .run(args)
diff --git a/examples/src/main/scala-3/com/geirolz/example/app/AppConfig.scala b/examples/src/main/scala/com/geirolz/example/app/AppConfig.scala
similarity index 100%
rename from examples/src/main/scala-3/com/geirolz/example/app/AppConfig.scala
rename to examples/src/main/scala/com/geirolz/example/app/AppConfig.scala
diff --git a/examples/src/main/scala/com/geirolz/example/app/AppDependencyServices.scala b/examples/src/main/scala/com/geirolz/example/app/AppDependencyServices.scala
new file mode 100644
index 0000000..bdc334d
--- /dev/null
+++ b/examples/src/main/scala/com/geirolz/example/app/AppDependencyServices.scala
@@ -0,0 +1,22 @@
+package com.geirolz.example.app
+
+import cats.effect.{IO, Resource}
+import com.geirolz.app.toolkit.{ctx, AppContext}
+import com.geirolz.example.app.provided.{HostTable, KafkaConsumer}
+import org.typelevel.log4cats.SelfAwareStructuredLogger
+
+case class AppDependencyServices(
+ kafkaConsumer: KafkaConsumer[IO],
+ hostTable: HostTable[IO]
+)
+object AppDependencyServices:
+
+ def resource(using
+ AppContext.NoDeps[AppInfo, SelfAwareStructuredLogger[IO], AppConfig, AppResources]
+ ): Resource[IO, AppDependencyServices] =
+ Resource.pure(
+ AppDependencyServices(
+ kafkaConsumer = KafkaConsumer.fake(ctx.config.kafkaBroker.host),
+ hostTable = HostTable.fromString(ctx.resources.hostTableValues)
+ )
+ )
diff --git a/examples/src/main/scala-2/com/geirolz/example/app/AppError.scala b/examples/src/main/scala/com/geirolz/example/app/AppError.scala
similarity index 85%
rename from examples/src/main/scala-2/com/geirolz/example/app/AppError.scala
rename to examples/src/main/scala/com/geirolz/example/app/AppError.scala
index 63e3344..05fbdb3 100644
--- a/examples/src/main/scala-2/com/geirolz/example/app/AppError.scala
+++ b/examples/src/main/scala/com/geirolz/example/app/AppError.scala
@@ -1,6 +1,5 @@
package com.geirolz.example.app
sealed trait AppError
-object AppError {
+object AppError:
case class UnknownError(message: String) extends AppError
-}
diff --git a/examples/src/main/scala-3/com/geirolz/example/app/AppInfo.scala b/examples/src/main/scala/com/geirolz/example/app/AppInfo.scala
similarity index 94%
rename from examples/src/main/scala-3/com/geirolz/example/app/AppInfo.scala
rename to examples/src/main/scala/com/geirolz/example/app/AppInfo.scala
index b760961..d1a14a6 100644
--- a/examples/src/main/scala-3/com/geirolz/example/app/AppInfo.scala
+++ b/examples/src/main/scala/com/geirolz/example/app/AppInfo.scala
@@ -18,7 +18,7 @@ object AppInfo:
given Show[AppInfo] = Show.fromToString
- val fromBuildInfo: AppInfo = {
+ val fromBuildInfo: AppInfo =
val builtOn: LocalDateTime = LocalDateTime.ofInstant(
Instant.ofEpochMilli(BuildInfo.builtAtMillis),
ZoneOffset.UTC
@@ -36,6 +36,3 @@ object AppInfo:
),
builtOn = builtOn
)
- }
-
-end AppInfo
diff --git a/examples/src/main/scala/com/geirolz/example/app/AppMain.scala b/examples/src/main/scala/com/geirolz/example/app/AppMain.scala
new file mode 100644
index 0000000..b48a42c
--- /dev/null
+++ b/examples/src/main/scala/com/geirolz/example/app/AppMain.scala
@@ -0,0 +1,35 @@
+package com.geirolz.example.app
+
+import cats.effect.IO
+import cats.syntax.all.given
+import com.geirolz.app.toolkit.{App, IOApp}
+import com.geirolz.app.toolkit.config.pureconfig.*
+import com.geirolz.app.toolkit.logger.given
+import com.geirolz.app.toolkit.novalues.{NoFailure, NoResources}
+import com.geirolz.example.app.provided.AppHttpServer
+import org.typelevel.log4cats.SelfAwareStructuredLogger
+import org.typelevel.log4cats.slf4j.Slf4jLogger
+
+object AppMain extends IOApp.Toolkit:
+ val app: App.Simple[IO, AppInfo, SelfAwareStructuredLogger, AppConfig, AppResources, AppDependencyServices] =
+ App[IO]
+ .withInfo(AppInfo.fromBuildInfo)
+ .withLoggerPure(Slf4jLogger.getLogger[IO])
+ .withConfigF(pureconfigLoader[IO, AppConfig])
+ .withResources(AppResources.resource)
+ .dependsOnE(AppDependencyServices.resource.map(_.asRight))
+ .beforeProviding(ctx.logger.info("CUSTOM PRE-RUN"))
+ .provideParallel(
+ List(
+ // HTTP server
+ AppHttpServer.resource(ctx.config).useForever,
+
+ // Kafka consumer
+ ctx.dependencies.kafkaConsumer
+ .consumeFrom("test-topic")
+ .evalTap(record => ctx.logger.info(s"Received record $record"))
+ .compile
+ .drain
+ )
+ )
+ .onFinalize(ctx.logger.info("CUSTOM END"))
diff --git a/examples/src/main/scala/com/geirolz/example/app/AppResources.scala b/examples/src/main/scala/com/geirolz/example/app/AppResources.scala
new file mode 100644
index 0000000..d358039
--- /dev/null
+++ b/examples/src/main/scala/com/geirolz/example/app/AppResources.scala
@@ -0,0 +1,17 @@
+package com.geirolz.example.app
+
+import cats.effect.{IO, Resource}
+
+import scala.io.Source
+
+case class AppResources(
+ hostTableValues: List[String]
+)
+
+object AppResources:
+ def resource: Resource[IO, AppResources] =
+ Resource
+ .fromAutoCloseable(IO(Source.fromResource("host-table.txt")))
+ .map(_.getLines().toList)
+ .map(AppResources(_))
+
diff --git a/examples/src/main/scala/com/geirolz/example/app/AppWithFailures.scala b/examples/src/main/scala/com/geirolz/example/app/AppWithFailures.scala
new file mode 100644
index 0000000..ffde9c5
--- /dev/null
+++ b/examples/src/main/scala/com/geirolz/example/app/AppWithFailures.scala
@@ -0,0 +1,35 @@
+package com.geirolz.example.app
+
+import cats.effect.IO
+import com.geirolz.app.toolkit.config.pureconfig.pureconfigLoader
+import com.geirolz.app.toolkit.logger.given
+import com.geirolz.app.toolkit.novalues.NoResources
+import com.geirolz.app.toolkit.{ctx, App, AppMessages, IOApp}
+import com.geirolz.example.app.provided.AppHttpServer
+import org.typelevel.log4cats.SelfAwareStructuredLogger
+import org.typelevel.log4cats.slf4j.Slf4jLogger
+import cats.syntax.all.*
+
+object AppWithFailures extends IOApp.Toolkit:
+ val app: App[IO, AppError, AppInfo, SelfAwareStructuredLogger, AppConfig, AppResources, AppDependencyServices] =
+ App[IO, AppError]
+ .withInfo(AppInfo.fromBuildInfo)
+ .withLoggerPure(Slf4jLogger.getLogger[IO])
+ .withConfigF(pureconfigLoader[IO, AppConfig])
+ .withResources(AppResources.resource)
+ .dependsOnE(AppDependencyServices.resource.map(_.asRight))
+ .beforeProviding(ctx.logger.info("CUSTOM PRE-RUN"))
+ .provideParallel(
+ List(
+ // HTTP server
+ AppHttpServer.resource(ctx.config).useForever,
+
+ // Kafka consumer
+ ctx.dependencies.kafkaConsumer
+ .consumeFrom("test-topic")
+ .evalTap(record => ctx.logger.info(s"Received record $record"))
+ .compile
+ .drain
+ )
+ )
+ .onFinalize(ctx.logger.info("CUSTOM END"))
diff --git a/examples/src/main/scala-3/com/geirolz/example/app/provided/AppHttpServer.scala b/examples/src/main/scala/com/geirolz/example/app/provided/AppHttpServer.scala
similarity index 100%
rename from examples/src/main/scala-3/com/geirolz/example/app/provided/AppHttpServer.scala
rename to examples/src/main/scala/com/geirolz/example/app/provided/AppHttpServer.scala
diff --git a/examples/src/main/scala/com/geirolz/example/app/provided/HostTable.scala b/examples/src/main/scala/com/geirolz/example/app/provided/HostTable.scala
new file mode 100644
index 0000000..b62c6fd
--- /dev/null
+++ b/examples/src/main/scala/com/geirolz/example/app/provided/HostTable.scala
@@ -0,0 +1,17 @@
+package com.geirolz.example.app.provided
+
+import cats.effect.IO
+
+trait HostTable[F[_]]:
+ def findByName(hostname: String): F[Option[String]]
+
+object HostTable:
+ def fromString(values: List[String]): HostTable[IO] = new HostTable[IO]:
+ private val map: Map[String, String] =
+ values.flatMap {
+ case s"$hostname:$ip" => Some(hostname -> ip)
+ case _ => None
+ }.toMap
+
+ def findByName(hostname: String): IO[Option[String]] =
+ IO.pure(map.get(hostname))
diff --git a/examples/src/main/scala-3/com/geirolz/example/app/provided/KafkaConsumer.scala b/examples/src/main/scala/com/geirolz/example/app/provided/KafkaConsumer.scala
similarity index 100%
rename from examples/src/main/scala-3/com/geirolz/example/app/provided/KafkaConsumer.scala
rename to examples/src/main/scala/com/geirolz/example/app/provided/KafkaConsumer.scala
diff --git a/integrations/fly4s/src/main/scala/com/geirolz/app/toolkit/fly4s/Fly4sAppMessages.scala b/integrations/fly4s/src/main/scala/com/geirolz/app/toolkit/fly4s/Fly4sAppMessages.scala
new file mode 100644
index 0000000..a80e76a
--- /dev/null
+++ b/integrations/fly4s/src/main/scala/com/geirolz/app/toolkit/fly4s/Fly4sAppMessages.scala
@@ -0,0 +1,11 @@
+package com.geirolz.app.toolkit.fly4s
+
+import fly4s.core.data.MigrateResult
+
+case class Fly4sAppMessages(
+ applyingMigrations: String = "Applying migrations to the database...",
+ failedToApplyMigrations: String = s"Unable to apply database migrations to database.",
+ successfullyApplied: MigrateResult => String = res => s"Applied ${res.migrationsExecuted} migrations to database."
+)
+object Fly4sAppMessages:
+ given Fly4sAppMessages = Fly4sAppMessages()
diff --git a/integrations/fly4s/src/main/scala/com/geirolz/app/toolkit/fly4s/package.scala b/integrations/fly4s/src/main/scala/com/geirolz/app/toolkit/fly4s/package.scala
deleted file mode 100644
index 92d5896..0000000
--- a/integrations/fly4s/src/main/scala/com/geirolz/app/toolkit/fly4s/package.scala
+++ /dev/null
@@ -1,59 +0,0 @@
-package com.geirolz.app.toolkit
-import _root_.fly4s.core.Fly4s
-import _root_.fly4s.core.data.Fly4sConfig
-import cats.effect.Resource
-import cats.effect.kernel.Async
-import com.geirolz.app.toolkit.logger.LoggerAdapter
-
-package object fly4s {
-
- import cats.syntax.all.*
-
- def migrateDatabaseWithConfig[F[_]: Async, APP_INFO <: SimpleAppInfo[?], LOGGER_T[_[_]]: LoggerAdapter, CONFIG, DEPENDENCIES, RESOURCES](
- url: CONFIG => String,
- user: CONFIG => Option[String] = (_: CONFIG) => None,
- password: CONFIG => Option[Array[Char]] = (_: CONFIG) => None,
- config: CONFIG => Fly4sConfig = (_: CONFIG) => Fly4sConfig.default,
- classLoader: ClassLoader = Thread.currentThread.getContextClassLoader
- ): App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] =
- migrateDatabaseWith(
- url = d => url(d.config),
- user = d => user(d.config),
- password = d => password(d.config),
- config = d => config(d.config),
- classLoader = classLoader
- )
-
- def migrateDatabaseWith[F[_]: Async, APP_INFO <: SimpleAppInfo[?], LOGGER_T[_[_]]: LoggerAdapter, CONFIG, DEPENDENCIES, RESOURCES](
- url: App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => String,
- user: App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => Option[String] = (_: Any) => None,
- password: App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => Option[Array[Char]] = (_: Any) => None,
- config: App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => Fly4sConfig = (_: Any) => Fly4sConfig.default,
- classLoader: ClassLoader = Thread.currentThread.getContextClassLoader
- ): App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] =
- migrateDatabase(dep =>
- Fly4s
- .make[F](
- url = url(dep),
- user = user(dep),
- password = password(dep),
- config = config(dep),
- classLoader = classLoader
- )
- )
-
- def migrateDatabase[F[_]: Async, APP_INFO <: SimpleAppInfo[?], LOGGER_T[_[_]]: LoggerAdapter, CONFIG, DEPENDENCIES, RESOURCES](
- f: App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => Resource[F, Fly4s[F]]
- ): App.Dependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] =
- dep =>
- f(dep)
- .evalMap(fl4s =>
- for {
- logger <- LoggerAdapter[LOGGER_T].toToolkit(dep.logger).pure[F]
- _ <- logger.debug(s"Applying migration to database...")
- result <- fl4s.migrate.onError(logger.error(_)(s"Unable to apply database migrations to database."))
- _ <- logger.info(s"Applied ${result.migrationsExecuted} migrations to database.")
- } yield ()
- )
- .use_
-}
diff --git a/integrations/fly4s/src/main/scala/com/geirolz/app/toolkit/fly4s/tasks.scala b/integrations/fly4s/src/main/scala/com/geirolz/app/toolkit/fly4s/tasks.scala
new file mode 100644
index 0000000..9f9fa82
--- /dev/null
+++ b/integrations/fly4s/src/main/scala/com/geirolz/app/toolkit/fly4s/tasks.scala
@@ -0,0 +1,39 @@
+package com.geirolz.app.toolkit.fly4s
+import _root_.fly4s.core.Fly4s
+import _root_.fly4s.core.data.Fly4sConfig
+import cats.effect.Resource
+import cats.effect.kernel.Async
+import cats.syntax.all.*
+import com.geirolz.app.toolkit.*
+import com.geirolz.app.toolkit.logger.LoggerAdapter
+
+def migrateDatabaseWith[F[_]: Async, INFO <: SimpleAppInfo[?], LOGGER_T[_[_]]: LoggerAdapter, CONFIG, DEPENDENCIES, RESOURCES](
+ url: String,
+ user: Option[String] = None,
+ password: Option[Array[Char]] = None,
+ config: Fly4sConfig = Fly4sConfig.default,
+ classLoader: ClassLoader = Thread.currentThread.getContextClassLoader
+)(using c: AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES], msgs: Fly4sAppMessages): F[Unit] =
+ migrateDatabase(
+ Fly4s.make[F](
+ url = url,
+ user = user,
+ password = password,
+ config = config,
+ classLoader = classLoader
+ )
+ )
+
+def migrateDatabase[F[_]: Async, INFO <: SimpleAppInfo[?], LOGGER_T[_[_]]: LoggerAdapter, CONFIG, DEPENDENCIES, RESOURCES](
+ fly4s: Resource[F, Fly4s[F]]
+)(using c: AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES], msgs: Fly4sAppMessages): F[Unit] =
+ fly4s
+ .evalMap(fl4s =>
+ for {
+ logger <- LoggerAdapter[LOGGER_T].toToolkit(ctx.logger).pure[F]
+ _ <- logger.debug(msgs.applyingMigrations)
+ result <- fl4s.migrate.onError(logger.error(_)(msgs.failedToApplyMigrations))
+ _ <- logger.info(msgs.successfullyApplied(result))
+ } yield ()
+ )
+ .use_
diff --git a/integrations/fly4s/src/test/scala/com/geirolz/app/toolkit/fly4s/Fly4sSupportSuite.scala b/integrations/fly4s/src/test/scala/com/geirolz/app/toolkit/fly4s/Fly4sSupportSuite.scala
index 964dbed..7754c82 100644
--- a/integrations/fly4s/src/test/scala/com/geirolz/app/toolkit/fly4s/Fly4sSupportSuite.scala
+++ b/integrations/fly4s/src/test/scala/com/geirolz/app/toolkit/fly4s/Fly4sSupportSuite.scala
@@ -2,9 +2,9 @@ package com.geirolz.app.toolkit.fly4s
import cats.effect.IO
import com.geirolz.app.toolkit.fly4s.testing.TestConfig
-import com.geirolz.app.toolkit.{App, SimpleAppInfo}
+import com.geirolz.app.toolkit.{ctx, App, AppMessages, SimpleAppInfo}
-class Fly4sSupportSuite extends munit.CatsEffectSuite {
+class Fly4sSupportSuite extends munit.CatsEffectSuite:
test("Syntax works as expected") {
App[IO]
@@ -16,7 +16,7 @@ class Fly4sSupportSuite extends munit.CatsEffectSuite {
sbtVersion = "1.8.0"
)
)
- .withConfig(
+ .withConfigPure(
TestConfig(
dbUrl = "jdbc:postgresql://localhost:5432/toolkit",
dbUser = Some("postgres"),
@@ -25,12 +25,11 @@ class Fly4sSupportSuite extends munit.CatsEffectSuite {
)
.withoutDependencies
.beforeProviding(
- migrateDatabaseWithConfig(
- url = _.dbUrl,
- user = _.dbUser,
- password = _.dbPassword
+ migrateDatabaseWith(
+ url = ctx.config.dbUrl,
+ user = ctx.config.dbUser,
+ password = ctx.config.dbPassword
)
)
- .provideOne(_ => IO.unit)
+ .provideOne(IO.unit)
}
-}
diff --git a/integrations/fly4s/src/test/scala/com/geirolz/app/toolkit/fly4s/testing/TestConfig.scala b/integrations/fly4s/src/test/scala/com/geirolz/app/toolkit/fly4s/testing/TestConfig.scala
index b8e204e..d14b213 100644
--- a/integrations/fly4s/src/test/scala/com/geirolz/app/toolkit/fly4s/testing/TestConfig.scala
+++ b/integrations/fly4s/src/test/scala/com/geirolz/app/toolkit/fly4s/testing/TestConfig.scala
@@ -3,6 +3,5 @@ package com.geirolz.app.toolkit.fly4s.testing
import cats.Show
case class TestConfig(dbUrl: String, dbUser: Option[String], dbPassword: Option[Array[Char]])
-object TestConfig {
- implicit val show: Show[TestConfig] = Show.fromToString
-}
+object TestConfig:
+ given Show[TestConfig] = Show.fromToString
diff --git a/integrations/log4cats/src/main/scala/com/geirolz/app/toolkit/logger/adapter.scala b/integrations/log4cats/src/main/scala/com/geirolz/app/toolkit/logger/adapter.scala
new file mode 100644
index 0000000..9fcaf78
--- /dev/null
+++ b/integrations/log4cats/src/main/scala/com/geirolz/app/toolkit/logger/adapter.scala
@@ -0,0 +1,20 @@
+package com.geirolz.app.toolkit.logger
+
+import org.typelevel.log4cats.Logger as Log4catsLogger
+
+given [LOG4S_LOGGER[F[_]] <: Log4catsLogger[F]]: LoggerAdapter[LOG4S_LOGGER] =
+ new LoggerAdapter[LOG4S_LOGGER]:
+ override def toToolkit[F[_]](u: LOG4S_LOGGER[F]): Logger[F] =
+ new Logger[F]:
+ override def error(message: => String): F[Unit] = u.error(message)
+ override def error(ex: Throwable)(message: => String): F[Unit] = u.error(ex)(message)
+ override def failure(message: => String): F[Unit] = u.error(message)
+ override def failure(ex: Throwable)(message: => String): F[Unit] = u.error(ex)(message)
+ override def warn(message: => String): F[Unit] = u.warn(message)
+ override def warn(ex: Throwable)(message: => String): F[Unit] = u.warn(ex)(message)
+ override def info(message: => String): F[Unit] = u.info(message)
+ override def info(ex: Throwable)(message: => String): F[Unit] = u.info(ex)(message)
+ override def debug(message: => String): F[Unit] = u.debug(message)
+ override def debug(ex: Throwable)(message: => String): F[Unit] = u.debug(ex)(message)
+ override def trace(message: => String): F[Unit] = u.trace(message)
+ override def trace(ex: Throwable)(message: => String): F[Unit] = u.trace(ex)(message)
diff --git a/integrations/log4cats/src/main/scala/com/geirolz/app/toolkit/logger/package.scala b/integrations/log4cats/src/main/scala/com/geirolz/app/toolkit/logger/package.scala
deleted file mode 100644
index 90ac091..0000000
--- a/integrations/log4cats/src/main/scala/com/geirolz/app/toolkit/logger/package.scala
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.geirolz.app.toolkit
-
-import org.typelevel.log4cats.Logger
-
-package object logger {
-
- implicit def log4CatsLoggerAdapter[LOG4S_LOGGER[F[_]] <: Logger[F]]: LoggerAdapter[LOG4S_LOGGER] =
- new LoggerAdapter[LOG4S_LOGGER] {
- override def toToolkit[F[_]](u: LOG4S_LOGGER[F]): ToolkitLogger[F] =
- new ToolkitLogger[F] {
- override def error(message: => String): F[Unit] = u.error(message)
- override def error(ex: Throwable)(message: => String): F[Unit] = u.error(ex)(message)
- override def warn(message: => String): F[Unit] = u.warn(message)
- override def warn(ex: Throwable)(message: => String): F[Unit] = u.warn(ex)(message)
- override def info(message: => String): F[Unit] = u.info(message)
- override def info(ex: Throwable)(message: => String): F[Unit] = u.info(ex)(message)
- override def debug(message: => String): F[Unit] = u.debug(message)
- override def debug(ex: Throwable)(message: => String): F[Unit] = u.debug(ex)(message)
- override def trace(message: => String): F[Unit] = u.trace(message)
- override def trace(ex: Throwable)(message: => String): F[Unit] = u.trace(ex)(message)
- }
- }
-}
diff --git a/integrations/log4cats/src/test/scala/com/geirolz/app/toolkit/logger/Log4CatsLoggerAdapterSuite.scala b/integrations/log4cats/src/test/scala/com/geirolz/app/toolkit/logger/Log4CatsLoggerAdapterSuite.scala
index ba60639..a46ec62 100644
--- a/integrations/log4cats/src/test/scala/com/geirolz/app/toolkit/logger/Log4CatsLoggerAdapterSuite.scala
+++ b/integrations/log4cats/src/test/scala/com/geirolz/app/toolkit/logger/Log4CatsLoggerAdapterSuite.scala
@@ -19,10 +19,11 @@ class Log4CatsLoggerAdapterSuite extends munit.CatsEffectSuite {
sbtVersion = "1.8.0"
)
)
- .withLogger(NoOpLogger[IO])
+ .withLoggerPure(NoOpLogger[IO])
.withoutDependencies
- .provideOne(_ => IO.unit)
- .run_
+ .provideOne(IO.unit)
+ .run()
+ .void
)
}
@@ -31,7 +32,7 @@ class Log4CatsLoggerAdapterSuite extends munit.CatsEffectSuite {
val tkLogger = adapterLogger.toToolkit(NoOpLogger[IO])
assertIO_(
- tkLogger.info("msg") >> tkLogger.error(ex"BOOM!")("msg")
+ tkLogger.info("msg") >> tkLogger.error(error"BOOM!")("msg")
)
}
}
diff --git a/integrations/odin/src/main/scala/com/geirolz/app/toolkit/logger/adapter.scala b/integrations/odin/src/main/scala/com/geirolz/app/toolkit/logger/adapter.scala
new file mode 100644
index 0000000..0d8bce9
--- /dev/null
+++ b/integrations/odin/src/main/scala/com/geirolz/app/toolkit/logger/adapter.scala
@@ -0,0 +1,20 @@
+package com.geirolz.app.toolkit.logger
+
+import io.odin.Logger as OdinLogger
+
+given [ODIN_LOGGER[F[_]] <: OdinLogger[F]]: LoggerAdapter[ODIN_LOGGER] =
+ new LoggerAdapter[ODIN_LOGGER]:
+ override def toToolkit[F[_]](u: ODIN_LOGGER[F]): Logger[F] =
+ new Logger[F]:
+ override def info(message: => String): F[Unit] = u.info(message)
+ override def info(ex: Throwable)(message: => String): F[Unit] = u.info(message, ex)
+ override def warn(message: => String): F[Unit] = u.warn(message)
+ override def warn(ex: Throwable)(message: => String): F[Unit] = u.warn(message, ex)
+ override def error(message: => String): F[Unit] = u.error(message)
+ override def error(ex: Throwable)(message: => String): F[Unit] = u.error(message, ex)
+ override def failure(message: => String): F[Unit] = u.error(message)
+ override def failure(ex: Throwable)(message: => String): F[Unit] = u.error(message, ex)
+ override def debug(message: => String): F[Unit] = u.debug(message)
+ override def debug(ex: Throwable)(message: => String): F[Unit] = u.debug(message, ex)
+ override def trace(message: => String): F[Unit] = u.trace(message)
+ override def trace(ex: Throwable)(message: => String): F[Unit] = u.trace(message, ex)
diff --git a/integrations/odin/src/main/scala/com/geirolz/app/toolkit/logger/package.scala b/integrations/odin/src/main/scala/com/geirolz/app/toolkit/logger/package.scala
deleted file mode 100644
index 2c790b7..0000000
--- a/integrations/odin/src/main/scala/com/geirolz/app/toolkit/logger/package.scala
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.geirolz.app.toolkit
-
-import io.odin.Logger
-
-package object logger {
-
- implicit def odinLoggerAdapter[ODIN_LOGGER[F[_]] <: Logger[F]]: LoggerAdapter[ODIN_LOGGER] =
- new LoggerAdapter[ODIN_LOGGER] {
- override def toToolkit[F[_]](u: ODIN_LOGGER[F]): ToolkitLogger[F] =
- new ToolkitLogger[F] {
- override def info(message: => String): F[Unit] = u.info(message)
- override def info(ex: Throwable)(message: => String): F[Unit] = u.info(message, ex)
- override def warn(message: => String): F[Unit] = u.warn(message)
- override def warn(ex: Throwable)(message: => String): F[Unit] = u.warn(message, ex)
- override def error(message: => String): F[Unit] = u.error(message)
- override def error(ex: Throwable)(message: => String): F[Unit] = u.error(message, ex)
- override def debug(message: => String): F[Unit] = u.debug(message)
- override def debug(ex: Throwable)(message: => String): F[Unit] = u.debug(message, ex)
- override def trace(message: => String): F[Unit] = u.trace(message)
- override def trace(ex: Throwable)(message: => String): F[Unit] = u.trace(message, ex)
- }
- }
-}
diff --git a/integrations/odin/src/test/scala/com/geirolz/app/toolkit/logger/OdinLoggerAdapterSuite.scala b/integrations/odin/src/test/scala/com/geirolz/app/toolkit/logger/OdinLoggerAdapterSuite.scala
index 35ab966..f9c336d 100644
--- a/integrations/odin/src/test/scala/com/geirolz/app/toolkit/logger/OdinLoggerAdapterSuite.scala
+++ b/integrations/odin/src/test/scala/com/geirolz/app/toolkit/logger/OdinLoggerAdapterSuite.scala
@@ -3,7 +3,7 @@ package com.geirolz.app.toolkit.logger
import cats.effect.IO
import com.geirolz.app.toolkit.{App, SimpleAppInfo}
import com.geirolz.app.toolkit.error.*
-import io.odin.Logger
+import io.odin.Logger as OdinLogger
class OdinLoggerAdapterSuite extends munit.CatsEffectSuite {
@@ -18,19 +18,20 @@ class OdinLoggerAdapterSuite extends munit.CatsEffectSuite {
sbtVersion = "1.8.0"
)
)
- .withLogger(Logger.noop[IO])
+ .withLoggerPure(OdinLogger.noop[IO])
.withoutDependencies
- .provideOne(_ => IO.unit)
- .run_
+ .provideOne(IO.unit)
+ .run()
+ .void
)
}
test("Implicit conversion with Logger") {
- val adapterLogger: LoggerAdapter[Logger] = implicitly[LoggerAdapter[Logger]]
- val tkLogger = adapterLogger.toToolkit(Logger.noop[IO])
+ val adapterLogger: LoggerAdapter[OdinLogger] = summon[LoggerAdapter[OdinLogger]]
+ val tkLogger = adapterLogger.toToolkit(OdinLogger.noop[IO])
assertIO_(
- tkLogger.info("msg") >> tkLogger.error(ex"BOOM!")("msg")
+ tkLogger.info("msg") >> tkLogger.error(error"BOOM!")("msg")
)
}
}
diff --git a/integrations/pureconfig/src/main/scala/com/geirolz/app/toolkit/config/package.scala b/integrations/pureconfig/src/main/scala/com/geirolz/app/toolkit/config/package.scala
deleted file mode 100644
index 2b54315..0000000
--- a/integrations/pureconfig/src/main/scala/com/geirolz/app/toolkit/config/package.scala
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.geirolz.app.toolkit
-
-import pureconfig.ConfigReader
-
-package object config {
-
- implicit def configReaderForSecret[T: ConfigReader: Secret.Obfuser]: ConfigReader[Secret[T]] =
- implicitly[ConfigReader[T]].map(t => Secret[T](t))
-}
diff --git a/integrations/pureconfig/src/main/scala/com/geirolz/app/toolkit/config/pureconfig/package.scala b/integrations/pureconfig/src/main/scala/com/geirolz/app/toolkit/config/pureconfig/package.scala
deleted file mode 100644
index 0375bf2..0000000
--- a/integrations/pureconfig/src/main/scala/com/geirolz/app/toolkit/config/pureconfig/package.scala
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.geirolz.app.toolkit.config
-
-import _root_.pureconfig.{ConfigObjectSource, ConfigReader, ConfigSource}
-import cats.effect.Async
-
-import scala.reflect.ClassTag
-
-package object pureconfig {
-
- def pureconfigLoader[F[_]: Async, PURE_CONFIG: ClassTag: ConfigReader]: F[PURE_CONFIG] =
- pureconfigLoader(_.default)
-
- def pureconfigLoader[F[_]: Async, PURE_CONFIG: ClassTag: ConfigReader](f: ConfigSource.type => ConfigObjectSource): F[PURE_CONFIG] =
- pureconfigLoader(f(ConfigSource))
-
- def pureconfigLoader[F[_]: Async, PURE_CONFIG: ClassTag: ConfigReader](appSource: ConfigObjectSource): F[PURE_CONFIG] =
- Async[F].delay(appSource.loadOrThrow[PURE_CONFIG])
-
-}
diff --git a/integrations/pureconfig/src/main/scala/com/geirolz/app/toolkit/config/pureconfig/tasks.scala b/integrations/pureconfig/src/main/scala/com/geirolz/app/toolkit/config/pureconfig/tasks.scala
new file mode 100644
index 0000000..beb13a0
--- /dev/null
+++ b/integrations/pureconfig/src/main/scala/com/geirolz/app/toolkit/config/pureconfig/tasks.scala
@@ -0,0 +1,15 @@
+package com.geirolz.app.toolkit.config.pureconfig
+
+import _root_.pureconfig.{ConfigObjectSource, ConfigReader, ConfigSource}
+import cats.effect.Async
+
+import scala.reflect.ClassTag
+
+def pureconfigLoader[F[_]: Async, PURE_CONFIG: ClassTag: ConfigReader]: F[PURE_CONFIG] =
+ pureconfigLoader(_.default)
+
+def pureconfigLoader[F[_]: Async, PURE_CONFIG: ClassTag: ConfigReader](f: ConfigSource.type => ConfigObjectSource): F[PURE_CONFIG] =
+ pureconfigLoader(f(ConfigSource))
+
+def pureconfigLoader[F[_]: Async, PURE_CONFIG: ClassTag: ConfigReader](appSource: ConfigObjectSource): F[PURE_CONFIG] =
+ Async[F].delay(appSource.loadOrThrow[PURE_CONFIG])
diff --git a/integrations/pureconfig/src/test/scala/com/geirolz/app/toolkit/config/PureconfigSecretSupportSuite.scala b/integrations/pureconfig/src/test/scala/com/geirolz/app/toolkit/config/PureconfigSecretSupportSuite.scala
deleted file mode 100644
index b46dceb..0000000
--- a/integrations/pureconfig/src/test/scala/com/geirolz/app/toolkit/config/PureconfigSecretSupportSuite.scala
+++ /dev/null
@@ -1,59 +0,0 @@
-package com.geirolz.app.toolkit.config
-
-import _root_.pureconfig.ConfigReader
-import _root_.pureconfig.backend.ConfigFactoryWrapper
-import cats.effect.IO
-import com.geirolz.app.toolkit.config.pureconfig.pureconfigLoader
-import com.geirolz.app.toolkit.config.testing.TestConfig
-import com.geirolz.app.toolkit.{App, SimpleAppInfo}
-import com.typesafe.config.Config
-
-class PureconfigSecretSupportSuite extends munit.CatsEffectSuite {
-
- test("Syntax works as expected") {
- assertIO_(
- App[IO]
- .withInfo(
- SimpleAppInfo.string(
- name = "toolkit",
- version = "0.0.1",
- scalaVersion = "2.13.10",
- sbtVersion = "1.8.0"
- )
- )
- .withConfigLoader(pureconfigLoader[IO, TestConfig])
- .withoutDependencies
- .provideOne(_ => IO.unit)
- .run_
- )
- }
-
- test("Read secret string with pureconfig") {
-
- val config: Config = ConfigFactoryWrapper
- .parseString(
- """
- |conf {
- | secret-value: "my-super-secret-password"
- |}""".stripMargin
- )
- .toOption
- .get
-
- val result: ConfigReader.Result[Secret[String]] = implicitly[ConfigReader[Secret[String]]].from(
- config.getValue("conf.secret-value")
- )
-
- assert(
- result
- .flatMap(_.useE(secretValue => {
- assertEquals(
- obtained = secretValue,
- expected = "my-super-secret-password"
- )
- }))
- .isRight
- )
-
- }
-}
diff --git a/project/ProjectDependencies.scala b/project/ProjectDependencies.scala
index afb99c4..417335e 100644
--- a/project/ProjectDependencies.scala
+++ b/project/ProjectDependencies.scala
@@ -50,7 +50,7 @@ object ProjectDependencies {
object Examples {
- private lazy val dedicatedCommon: Seq[ModuleID] = Seq(
+ lazy val dedicated: Seq[ModuleID] = Seq(
// http
"org.http4s" %% "http4s-dsl" % http4sVersion,
"org.http4s" %% "http4s-ember-server" % http4sVersion,
@@ -69,15 +69,7 @@ object ProjectDependencies {
// json
"io.circe" %% "circe-core" % circeVersion,
- "io.circe" %% "circe-refined" % circeVersion
- )
-
- lazy val dedicated_2_13: Seq[ModuleID] = dedicatedCommon ++ Seq(
- "com.github.pureconfig" %% "pureconfig-generic" % pureConfigVersion,
- "io.circe" %% "circe-generic-extras" % circeGenericExtraVersion
- )
-
- lazy val dedicated_3_2: Seq[ModuleID] = dedicatedCommon ++ Seq(
+ "io.circe" %% "circe-refined" % circeVersion,
"io.circe" %% "circe-generic" % circeVersion
)
}
@@ -111,16 +103,10 @@ object ProjectDependencies {
}
object Plugins {
- val compilerPluginsFor2_13: Seq[ModuleID] = Seq(
- compilerPlugin("org.typelevel" %% "kind-projector" % "0.13.2" cross CrossVersion.full),
- compilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.1")
- )
-
- val compilerPluginsFor3: Seq[ModuleID] = Nil
+ val compilerPlugins: Seq[ModuleID] = Nil
}
object Docs {
- lazy val dedicated_2_13: Seq[ModuleID] = Examples.dedicated_2_13
- lazy val dedicated_3_2: Seq[ModuleID] = Examples.dedicated_3_2
+ lazy val dedicated: Seq[ModuleID] = Examples.dedicated
}
}
diff --git a/project/plugins.sbt b/project/plugins.sbt
index ddfd295..3803aea 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -1,7 +1,7 @@
import sbt.addSbtPlugin
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2")
-//addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.8")
+addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.9")
addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.12")
addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.1")
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0")
diff --git a/testing/src/main/scala/com/geirolz/app/toolkit/testing/EventLogger.scala b/testing/src/main/scala/com/geirolz/app/toolkit/testing/EventLogger.scala
index b097a3d..4d90632 100644
--- a/testing/src/main/scala/com/geirolz/app/toolkit/testing/EventLogger.scala
+++ b/testing/src/main/scala/com/geirolz/app/toolkit/testing/EventLogger.scala
@@ -4,24 +4,25 @@ import cats.Functor
import cats.effect.kernel.MonadCancelThrow
import cats.effect.{Ref, Resource}
-class EventLogger[F[_]](ref: Ref[F, List[Event]]) {
+class EventLogger[F[_]](ref: Ref[F, List[Event]]):
- def events: F[List[Event]] = ref.get
+ def events: F[List[Event]] =
+ ref.get
def append(event: Event): F[Unit] =
ref.update(_ :+ event)
-}
-object EventLogger {
+
+object EventLogger:
import cats.syntax.all.*
- def apply[F[_]: EventLogger]: EventLogger[F] = implicitly[EventLogger[F]]
+ inline def apply[F[_]: EventLogger]: EventLogger[F] =
+ summon[EventLogger[F]]
- def create[F[_]: Ref.Make: Functor]: F[EventLogger[F]] = {
+ def create[F[_]: Ref.Make: Functor]: F[EventLogger[F]] =
Ref.of(List.empty[Event]).map(new EventLogger(_))
- }
- implicit class appLoaderResOps[F[+_]: MonadCancelThrow: EventLogger](compiledApp: Resource[F, F[Unit]]) {
+ extension [F[+_]: MonadCancelThrow: EventLogger](compiledApp: Resource[F, F[Unit]])
def traceAsAppLoader: Resource[F, F[Unit]] =
compiledApp.trace(LabeledResource.appLoader)
@@ -30,16 +31,13 @@ object EventLogger {
compiledApp.traceAsAppLoader
.map(_.traceAsAppRuntime)
.useEval
- }
- implicit class appRuntimeResOps[F[_]: MonadCancelThrow: EventLogger](app: F[Unit]) {
+ extension [F[_]: MonadCancelThrow: EventLogger](app: F[Unit])
def traceAsAppRuntime: F[Unit] =
Resource.eval(app).trace(LabeledResource.appRuntime).use_
- }
-
- implicit class genericResOps[F[_]: MonadCancelThrow: EventLogger, T](resource: Resource[F, T]) {
- def trace(labeledResource: LabeledResource): Resource[F, T] = {
+ extension [F[_]: MonadCancelThrow: EventLogger, T](resource: Resource[F, T])
+ def trace(labeledResource: LabeledResource): Resource[F, T] =
val logger = EventLogger[F]
resource
.preAllocate(logger.append(labeledResource.starting))
@@ -47,6 +45,3 @@ object EventLogger {
.flatTap(_ => Resource.eval(logger.append(labeledResource.succeeded)))
.onError(e => Resource.eval(logger.append(labeledResource.errored(e.getMessage))))
.onCancel(Resource.eval(logger.append(labeledResource.canceled)))
- }
- }
-}
diff --git a/testing/src/main/scala/com/geirolz/app/toolkit/testing/events.scala b/testing/src/main/scala/com/geirolz/app/toolkit/testing/events.scala
index 6cd673d..c3e371d 100644
--- a/testing/src/main/scala/com/geirolz/app/toolkit/testing/events.scala
+++ b/testing/src/main/scala/com/geirolz/app/toolkit/testing/events.scala
@@ -3,37 +3,36 @@ package com.geirolz.app.toolkit.testing
import java.util.UUID
sealed trait Event
-object Event {
+object Event:
case class Custom(key: String) extends Event
-}
-sealed trait LabelEvent extends Event {
+
+sealed trait LabelEvent extends Event:
val resource: LabeledResource
- override def toString: String = this match {
- case LabelEvent.Starting(resource) => s"${resource.label}-starting"
- case LabelEvent.Succeeded(resource) => s"${resource.label}-succeeded"
- case LabelEvent.Finalized(resource) => s"${resource.label}-finalized"
- case LabelEvent.Canceled(resource) => s"${resource.label}-canceled"
- case LabelEvent.Errored(resource, msg) => s"${resource.label}-errored[$msg]"
- }
-}
-object LabelEvent {
+ override def toString: String =
+ this match
+ case LabelEvent.Starting(resource) => s"${resource.label}-starting"
+ case LabelEvent.Succeeded(resource) => s"${resource.label}-succeeded"
+ case LabelEvent.Finalized(resource) => s"${resource.label}-finalized"
+ case LabelEvent.Canceled(resource) => s"${resource.label}-canceled"
+ case LabelEvent.Errored(resource, msg) => s"${resource.label}-errored[$msg]"
+
+object LabelEvent:
case class Starting(resource: LabeledResource) extends LabelEvent
case class Succeeded(resource: LabeledResource) extends LabelEvent
case class Finalized(resource: LabeledResource) extends LabelEvent
case class Canceled(resource: LabeledResource) extends LabelEvent
case class Errored(resource: LabeledResource, msg: String) extends LabelEvent
-}
//------------------------------------------------------
-case class LabeledResource(label: String, token: UUID) {
+case class LabeledResource(label: String, token: UUID):
def starting: LabelEvent = LabelEvent.Starting(this)
def succeeded: LabelEvent = LabelEvent.Succeeded(this)
def finalized: LabelEvent = LabelEvent.Finalized(this)
def canceled: LabelEvent = LabelEvent.Canceled(this)
def errored(msg: String): LabelEvent = LabelEvent.Errored(this, msg)
-}
-object LabeledResource {
+
+object LabeledResource:
private val resourceToken: UUID = UUID.randomUUID()
private[LabeledResource] def apply(id: String, token: UUID): LabeledResource = new LabeledResource(id, token)
def resource(id: String): LabeledResource = LabeledResource(id, resourceToken)
@@ -41,5 +40,5 @@ object LabeledResource {
val http: LabeledResource = LabeledResource.uniqueResource("http")
val appRuntime: LabeledResource = LabeledResource.uniqueResource("app-runtime")
val appLoader: LabeledResource = LabeledResource.uniqueResource("app-loader")
+ val appResources: LabeledResource = LabeledResource.uniqueResource("app-resources")
val appDependencies: LabeledResource = LabeledResource.uniqueResource("app-dependencies")
-}