From 3376af9583f886411f396bf468ab862e957034fa Mon Sep 17 00:00:00 2001 From: geirolz Date: Thu, 18 Jan 2024 14:54:12 +0100 Subject: [PATCH 01/22] Init setup --- .github/workflows/cicd.yml | 5 +-- .mergify.yml | 3 -- .scalafmt.conf | 8 +--- build.sbt | 73 +++++-------------------------- project/ProjectDependencies.scala | 17 ++----- 5 files changed, 16 insertions(+), 90 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 2c2e12c..01c6010 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -28,12 +28,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 + test-tasks: scalafmtCheck test gen-doc steps: - uses: actions/checkout@v3 diff --git a/.mergify.yml b/.mergify.yml index b45b4fb..ec5be09 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -4,7 +4,6 @@ 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) - base=main - label!=work-in-progress @@ -14,7 +13,6 @@ 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) - base=main actions: @@ -23,7 +21,6 @@ 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) - base=main actions: 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/build.sbt b/build.sbt index ad16903..6e43ae7 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 scala33 = "3.3.1" +lazy val supportedScalaVersions = List(scala33) //## global project to no publish ## val copyReadMe = taskKey[Unit]("Copy generated README to main folder.") @@ -214,75 +213,25 @@ 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. "-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/project/ProjectDependencies.scala b/project/ProjectDependencies.scala index afb99c4..bcd40d7 100644 --- a/project/ProjectDependencies.scala +++ b/project/ProjectDependencies.scala @@ -72,12 +72,7 @@ object ProjectDependencies { "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( + lazy val dedicated_3: Seq[ModuleID] = dedicatedCommon ++ Seq( "io.circe" %% "circe-generic" % circeVersion ) } @@ -111,16 +106,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_3 } } From ce6b7b4844cc32b892fce8b6eaa6278d5401ecd0 Mon Sep 17 00:00:00 2001 From: geirolz Date: Thu, 18 Jan 2024 15:33:29 +0100 Subject: [PATCH 02/22] First iteration --- build.sbt | 32 +- .../app/toolkit/config/BytesUtils.scala | 20 - .../geirolz/app/toolkit/config/Secret.scala | 414 ------------------ .../app/toolkit/config/BytesUtilsSuite.scala | 29 -- .../app/toolkit/config/SecretSuite.scala | 142 ------ .../app/toolkit/config/testing/Gens.scala | 8 - .../app/toolkit/config/testing/Timed.scala | 14 - .../com/geirolz/app/toolkit/AppArgs.scala | 53 +-- .../geirolz/app/toolkit/AppInterpreter.scala | 9 +- .../com/geirolz/app/toolkit/AppMessages.scala | 30 +- .../geirolz/app/toolkit/FailureHandler.scala | 7 +- .../geirolz/app/toolkit/SimpleAppInfo.scala | 12 +- .../app/toolkit/TypeInequalities.scala | 4 +- .../app/toolkit/console/AnsiValue.scala | 86 ++-- .../app/toolkit/error/ErrorLifter.scala | 25 +- .../app/toolkit/error/MultiError.scala | 10 +- .../app/toolkit/error/MultiException.scala | 18 +- .../geirolz/app/toolkit/error/implicits.scala | 21 + .../geirolz/app/toolkit/error/package.scala | 28 -- .../app/toolkit/logger/LoggerAdapter.scala | 34 +- .../app/toolkit/logger/NoopLogger.scala | 27 +- .../app/toolkit/logger/ToolkitLogger.scala | 144 +++--- .../app/toolkit/novalues/NoConfig.scala | 7 +- .../app/toolkit/novalues/NoDependencies.scala | 3 +- .../app/toolkit/novalues/NoResources.scala | 3 +- .../com/geirolz/app/toolkit/package.scala | 5 - .../scala/com/geirolz/app/toolkit/types.scala | 6 + project/ProjectDependencies.scala | 9 +- 28 files changed, 252 insertions(+), 948 deletions(-) delete mode 100644 config/src/main/scala/com/geirolz/app/toolkit/config/BytesUtils.scala delete mode 100644 config/src/main/scala/com/geirolz/app/toolkit/config/Secret.scala delete mode 100644 config/src/test/scala/com/geirolz/app/toolkit/config/BytesUtilsSuite.scala delete mode 100644 config/src/test/scala/com/geirolz/app/toolkit/config/SecretSuite.scala delete mode 100644 config/src/test/scala/com/geirolz/app/toolkit/config/testing/Gens.scala delete mode 100644 config/src/test/scala/com/geirolz/app/toolkit/config/testing/Timed.scala create mode 100644 core/src/main/scala/com/geirolz/app/toolkit/error/implicits.scala delete mode 100644 core/src/main/scala/com/geirolz/app/toolkit/error/package.scala delete mode 100644 core/src/main/scala/com/geirolz/app/toolkit/package.scala create mode 100644 core/src/main/scala/com/geirolz/app/toolkit/types.scala diff --git a/build.sbt b/build.sbt index 6e43ae7..9405d3f 100644 --- a/build.sbt +++ b/build.sbt @@ -34,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"), @@ -71,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", @@ -95,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, @@ -117,7 +97,7 @@ lazy val examples: Project = { ), buildInfoPackage := appPackage ) - .dependsOn(core, config, log4cats, pureconfig) + .dependsOn(core, log4cats, pureconfig) } // integrations @@ -144,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 ) 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/AppArgs.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppArgs.scala index 0eb4951..c8742dd 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppArgs.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppArgs.scala @@ -5,7 +5,7 @@ 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 = (p +: pN).forall(_.apply(this)) @@ -69,49 +69,44 @@ final case class AppArgs(private val value: List[String]) extends AnyVal { private 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: + 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 { + +object ArgDecoder: def apply[T: ArgDecoder]: ArgDecoder[T] = implicitly[ArgDecoder[T]] def fromTry[T](t: String => Try[T]): ArgDecoder[T] = (value: String) => t(value).toEither.left.map(ArgDecodingException(_)) - sealed trait ArgDecodingError { + sealed trait ArgDecodingError: 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/AppInterpreter.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppInterpreter.scala index 12d57a7..741c154 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppInterpreter.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppInterpreter.scala @@ -8,7 +8,7 @@ import cats.effect.{Async, Fiber, Ref, Resource} import com.geirolz.app.toolkit.FailureHandler.OnFailureBehaviour import com.geirolz.app.toolkit.logger.LoggerAdapter -trait AppInterpreter[F[+_]] { +trait AppInterpreter[F[+_]]: def run[T](compiledApp: Resource[F, F[T]])(implicit F: MonadCancelThrow[F]): F[T] @@ -23,14 +23,14 @@ trait AppInterpreter[F[+_]] { F: Async[F], P: Parallel[F] ): Resource[F, FAILURE \/ F[NonEmptyList[FAILURE] \/ Unit]] -} -object AppInterpreter { + +object AppInterpreter: import cats.syntax.all.* def apply[F[+_]](implicit ac: AppInterpreter[F]): AppInterpreter[F] = ac - implicit def default[F[+_]]: AppInterpreter[F] = new AppInterpreter[F] { + given [F[+_]]: AppInterpreter[F] = new AppInterpreter[F] { override def run[T](compiledApp: Resource[F, F[T]])(implicit F: MonadCancelThrow[F]): F[T] = compiledApp.useEval @@ -123,4 +123,3 @@ object AppInterpreter { } ).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..faee952 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppMessages.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppMessages.scala @@ -12,24 +12,24 @@ case class AppMessages( appEnErrorOccurred: String, shuttingDownApp: String ) -object AppMessages { +object AppMessages: def fromAppInfo[APP_INFO <: SimpleAppInfo[?]](info: APP_INFO)( f: APP_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 = + 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}..." + ) ) - ) -} diff --git a/core/src/main/scala/com/geirolz/app/toolkit/FailureHandler.scala b/core/src/main/scala/com/geirolz/app/toolkit/FailureHandler.scala index 14c64a2..ce2ee39 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/FailureHandler.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/FailureHandler.scala @@ -29,7 +29,7 @@ case class FailureHandler[F[_], FAILURE]( ): FailureHandler[F, NonEmptyList[EE]] = this.asInstanceOf[FailureHandler[F, NonEmptyList[EE]]] } -object FailureHandler extends FailureHandlerSyntax { +object FailureHandler extends FailureHandlerSyntax: def summon[F[_], E](implicit ev: FailureHandler[F, E]): FailureHandler[F, E] = ev @@ -40,11 +40,10 @@ object FailureHandler extends FailureHandlerSyntax { ) sealed trait OnFailureBehaviour - object OnFailureBehaviour { + object OnFailureBehaviour: case object CancelAll extends OnFailureBehaviour case object DoNothing extends OnFailureBehaviour - } -} + sealed trait FailureHandlerSyntax { import cats.syntax.all.* 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 index 2c17c4c..db080ec 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/TypeInequalities.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/TypeInequalities.scala @@ -8,9 +8,9 @@ import scala.annotation.implicitNotFound sealed trait =:!=[A, B] //noinspection ScalaFileName -object =:!= { +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..c479e99 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 @@ -25,30 +25,30 @@ import com.geirolz.app.toolkit.console.AnsiValue.AnsiText * .withStyle(AnsiValue.S.BLINK) * }}} */ -sealed trait AnsiValue { +sealed trait AnsiValue: val value: String def apply[T](msg: T)(implicit 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 +69,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 +77,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 +98,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) @@ -175,10 +171,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) @@ -232,10 +227,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 +270,26 @@ 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 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 { - - implicit class AnsiTextOps(t: AnsiText) { + given Show[AnsiValue] = Show.fromToString - def print[F[_]: Console]: F[Unit] = Console[F].print(t) +private[toolkit] sealed 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 +314,4 @@ 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..3375d28 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(_)) + 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(_)) - 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..19194f7 --- /dev/null +++ b/core/src/main/scala/com/geirolz/app/toolkit/error/implicits.scala @@ -0,0 +1,21 @@ +package com.geirolz.app.toolkit.error + +import cats.kernel.Semigroup + +extension (ctx: StringContext) + def ex(args: Any*): RuntimeException = + new RuntimeException(ctx.s(args*)).dropFirstStackTraceElement + +extension [T <: Throwable](ex: T) + def dropFirstStackTraceElement: T = + val stackTrace = ex.getStackTrace + if (stackTrace != null && stackTrace.length > 1) + ex.setStackTrace(stackTrace.tail) + ex + +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/logger/LoggerAdapter.scala b/core/src/main/scala/com/geirolz/app/toolkit/logger/LoggerAdapter.scala index 6423f47..e34d99c 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,22 @@ package com.geirolz.app.toolkit.logger -trait LoggerAdapter[LOGGER[_[_]]] { +trait LoggerAdapter[LOGGER[_[_]]]: def toToolkit[F[_]](appLogger: LOGGER[F]): ToolkitLogger[F] -} -object LoggerAdapter { + +object LoggerAdapter: def apply[LOGGER[_[_]]: LoggerAdapter]: LoggerAdapter[LOGGER] = implicitly[LoggerAdapter[LOGGER]] 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) - } - } -} + 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) 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..8f7ea05 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 @@ -3,17 +3,16 @@ 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 - } -} +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 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 index 3438062..f99e3c4 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/logger/ToolkitLogger.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/logger/ToolkitLogger.scala @@ -9,7 +9,7 @@ import com.geirolz.app.toolkit.console.AnsiValue.AnsiText import java.io.PrintStream -trait ToolkitLogger[F[_]] { +trait ToolkitLogger[F[_]]: def error(message: => String): F[Unit] def error(ex: Throwable)(message: => String): F[Unit] def warn(message: => String): F[Unit] @@ -21,90 +21,86 @@ trait ToolkitLogger[F[_]] { 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 { + +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 { + 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 - } + given Show[Level] = Show.fromToString + given Order[Level] = Order.by(_.index) - 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 - } + 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)) - val formattedMsg: AnsiText = - color(s"[${appInfo.name.toString.toLowerCase}] $level - $message") + private def log(level: Level, message: => String, ex: Option[Throwable] = None): F[Unit] = + Async[F].whenA(level >= minLevel) { - Async[F].delay(ps.println(formattedMsg)).flatMap { _ => - ex match { - case Some(e) => Async[F].delay { e.printStackTrace(ps) } - case None => Async[F].unit + 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)) - } -} + + 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..1074538 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 { +object NoDependencies: final val value: NoDependencies = new NoDependencies {} -} 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..57c54b5 --- /dev/null +++ b/core/src/main/scala/com/geirolz/app/toolkit/types.scala @@ -0,0 +1,6 @@ +package com.geirolz.app.toolkit + +import scala.annotation.targetName + +@targetName("Either") +type \/[+A, +B] = Either[A, B] diff --git a/project/ProjectDependencies.scala b/project/ProjectDependencies.scala index bcd40d7..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,10 +69,7 @@ object ProjectDependencies { // json "io.circe" %% "circe-core" % circeVersion, - "io.circe" %% "circe-refined" % circeVersion - ) - - lazy val dedicated_3: Seq[ModuleID] = dedicatedCommon ++ Seq( + "io.circe" %% "circe-refined" % circeVersion, "io.circe" %% "circe-generic" % circeVersion ) } @@ -110,6 +107,6 @@ object ProjectDependencies { } object Docs { - lazy val dedicated: Seq[ModuleID] = Examples.dedicated_3 + lazy val dedicated: Seq[ModuleID] = Examples.dedicated } } From a5e5ed44d4069cc2f1ebf7d7d908d16cbce15cc9 Mon Sep 17 00:00:00 2001 From: geirolz Date: Thu, 18 Jan 2024 17:03:05 +0100 Subject: [PATCH 03/22] Second iteration --- README.md | 2 +- .../scala/com/geirolz/app/toolkit/App.scala | 328 ++++++------------ ...AppInterpreter.scala => AppCompiler.scala} | 12 +- .../geirolz/app/toolkit/AppDependencies.scala | 48 +++ .../geirolz/app/toolkit/AppResources.scala | 52 +++ .../app/toolkit/TypeInequalities.scala | 16 - .../AppResourcesAndDependenciesSuite.scala | 8 +- docs/compiled/README.md | 2 +- docs/source/README.md | 2 +- .../example/app/AppDependencyServices.scala | 2 +- .../example/app/AppDependencyServices.scala | 2 +- .../geirolz/app/toolkit/fly4s/package.scala | 16 +- 12 files changed, 220 insertions(+), 270 deletions(-) rename core/src/main/scala/com/geirolz/app/toolkit/{AppInterpreter.scala => AppCompiler.scala} (93%) create mode 100644 core/src/main/scala/com/geirolz/app/toolkit/AppDependencies.scala create mode 100644 core/src/main/scala/com/geirolz/app/toolkit/AppResources.scala delete mode 100644 core/src/main/scala/com/geirolz/app/toolkit/TypeInequalities.scala diff --git a/README.md b/README.md index 62204c3..db23f59 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ object Config { case class AppDependencyServices(kafkaConsumer: KafkaConsumer[IO]) object AppDependencyServices { - def resource(res: App.Resources[SimpleAppInfo[String], ToolkitLogger[IO], Config, NoResources]): Resource[IO, AppDependencyServices] = + def resource(res: AppResources[SimpleAppInfo[String], ToolkitLogger[IO], Config, NoResources]): Resource[IO, AppDependencyServices] = Resource.pure(AppDependencyServices(KafkaConsumer.fake)) } 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..fb4d60f 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/App.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/App.scala @@ -8,6 +8,8 @@ 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 scala.util.NotGiven + class App[ F[+_]: Async: Parallel, FAILURE, @@ -22,61 +24,59 @@ class App[ 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]]] -) { + val beforeProvidingF: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit], + val onFinalizeF: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit], + val failureHandlerLoader: AppResources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => FailureHandler[F, FAILURE], + val dependenciesLoader: AppResources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, FAILURE \/ DEPENDENCIES], + val provideBuilder: AppDependencies[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] + type Self = App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] + type Resources = AppResources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] - class Resourced[T] private (val appRes: AppResources, val value: T) { + class Resourced[T] private (val appRes: Resources, 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 map[U](f: T => U): Resourced[U] = Resourced(appRes, f(value)) + def tupled: (Resources, T) = Resourced.unapply(this) + def useTupled[U](f: (Resources, T) => U): U = f.tupled(tupled) + def use[U](f: Resources => 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) - } + + object Resourced: + def apply[T](appRes: Resources, value: T): Resourced[T] = new Resourced[T](appRes, value) + def unapply[T](r: Resourced[T]): (Resources, 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) + f: AppDependencies[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]* + f: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit], + fN: AppDependencies[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]] + f: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => G[F[Unit]] ): Self = copyWith(onFinalizeF = f(_).sequence_) private[toolkit] def _updateFailureHandlerLoader( - fh: App.Resources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => Endo[FailureHandler[F, FAILURE]] + fh: AppResources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => Endo[FailureHandler[F, FAILURE]] ): App[ F, FAILURE, @@ -99,24 +99,16 @@ 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.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: AppDependencies[APP_INFO2, LOGGER_T2[G], CONFIG2, DEPS2, RES2] => G[Unit] = this.beforeProvidingF, + onFinalizeF: AppDependencies[APP_INFO2, LOGGER_T2[G], CONFIG2, DEPS2, RES2] => G[Unit] = this.onFinalizeF, + failureHandlerLoader: AppResources[APP_INFO2, LOGGER_T2[G], CONFIG2, RES2] => FailureHandler[G, FAILURE2] = this.failureHandlerLoader, + dependenciesLoader: AppResources[APP_INFO2, LOGGER_T2[G], CONFIG2, RES2] => Resource[G, FAILURE2 \/ DEPS2] = this.dependenciesLoader, + provideBuilder: AppDependencies[APP_INFO2, LOGGER_T2[G], CONFIG2, DEPS2, RES2] => G[FAILURE2 \/ List[G[FAILURE2 \/ Any]]] = this.provideBuilder ): App[G, FAILURE2, APP_INFO2, LOGGER_T2, CONFIG2, RES2, DEPS2] = new App[G, FAILURE2, APP_INFO2, LOGGER_T2, CONFIG2, RES2, DEPS2]( appInfo = appInfo, @@ -130,118 +122,18 @@ class App[ dependenciesLoader = dependenciesLoader, provideBuilder = 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 - ) +object App extends AppSyntax: - 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 - ) - ) - } + import cats.syntax.all.* - def apply[F[+_]: Async: Parallel](implicit dummyImplicit: DummyImplicit): AppBuilderRuntimeSelected[F, Throwable] = + def apply[F[+_]: Async: Parallel](using 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] () { + 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] = @@ -251,18 +143,20 @@ object App extends AppSyntax { 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] ( + 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] = @@ -294,7 +188,7 @@ object App extends AppSyntax { def withConfigLoader[CONFIG2: Show]( configLoader: APP_INFO => F[CONFIG2] - )(implicit dummyImplicit: DummyImplicit): AppBuilderSelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG2, RESOURCES] = + )(using DummyImplicit): AppBuilderSelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG2, RESOURCES] = withConfigLoader(i => Resource.eval(configLoader(i))) def withConfigLoader[CONFIG2: Show]( @@ -336,13 +230,13 @@ object App extends AppSyntax { dependsOn[NoDependencies, FAILURE](_ => Resource.pure(NoDependencies.value.asRight[FAILURE])) def dependsOn[DEPENDENCIES]( - f: App.Resources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, DEPENDENCIES] + f: AppResources[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] = + f: AppResources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, FAILURE2 \/ DEPENDENCIES] + )(using DummyImplicit): AppBuilderSelectProvide[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = AppBuilderSelectProvide( appInfo = appInfo, loggerBuilder = loggerBuilder, @@ -352,9 +246,7 @@ object App extends AppSyntax { beforeProvidingF = _ => ().pure[F] ) - private def copyWith[G[+_]: Async: Parallel, ERROR2, APP_INFO2 <: SimpleAppInfo[?], LOGGER_T2[ - _[_] - ]: LoggerAdapter, CONFIG2: Show, RESOURCES2]( + 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, @@ -365,7 +257,6 @@ object App extends AppSyntax { configLoader = configLoader, resourcesLoader = resourcesLoader ) - } final case class AppBuilderSelectProvide[ F[+_]: Async: Parallel, @@ -380,72 +271,67 @@ object App extends AppSyntax { 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] - ) { + private val dependenciesLoader: AppResources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, FAILURE \/ DEPENDENCIES], + private val beforeProvidingF: AppDependencies[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] + f: AppDependencies[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]* + f: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit], + fN: AppDependencies[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]] + f: AppDependencies[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] = + f: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Any] + )(using 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] + f: AppDependencies[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]] + f: AppDependencies[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]] + f: AppDependencies[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] = + f: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => List[F[FAILURE2 \/ Any]] + )(using 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]]] + f: AppDependencies[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] = + f: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[List[F[FAILURE2 \/ Any]]] + )(using 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]]] + f: AppDependencies[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, @@ -459,8 +345,8 @@ object App extends AppSyntax { dependenciesLoader = dependenciesLoader, provideBuilder = f ) - } - object AppBuilderSelectProvide { + + object AppBuilderSelectProvide: private[App] def apply[ F[+_]: Async: Parallel, FAILURE, @@ -474,11 +360,8 @@ object App extends AppSyntax { 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] + dependenciesLoader: AppResources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, FAILURE \/ DEPENDENCIES], + beforeProvidingF: AppDependencies[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, @@ -488,31 +371,21 @@ object App extends AppSyntax { dependenciesLoader = dependenciesLoader, beforeProvidingF = beforeProvidingF ) - } -} -sealed trait AppSyntax { + +end App + +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) { + extension [F[+_]: Async: Parallel, FAILURE, APP_INFO <: SimpleAppInfo[?], LOGGER_T[_[_]]: LoggerAdapter, CONFIG: Show, RESOURCES, DEPENDENCIES]( + app: App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] + )(using NotGiven[FAILURE =:= Throwable]) { // failures def mapFailure[FAILURE2]( - fhLoader: App.Resources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => FailureHandler[ - F, - FAILURE2 - ] - )( - f: FAILURE => FAILURE2 - ): App[F, FAILURE2, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + fhLoader: AppResources[APP_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]( failureHandlerLoader = fhLoader, dependenciesLoader = app.dependenciesLoader.andThen(_.map(_.leftMap(f))), @@ -537,19 +410,16 @@ sealed trait AppSyntax { 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 compile(appArgs: List[String] = Nil)(using AppCompiler[F]): Resource[F, FAILURE \/ F[NonEmptyList[FAILURE] \/ Unit]] = + AppCompiler[F].compile(appArgs, app) - def run(appArgs: List[String] = Nil)(implicit c: AppInterpreter[F]): F[ExitCode] = + def run(appArgs: List[String] = Nil)(using AppCompiler[F]): F[ExitCode] = runMap[ExitCode](appArgs).apply { case Left(_) => ExitCode.Error case Right(_) => ExitCode.Success } - def runReduce[B](appArgs: List[String] = Nil, f: FAILURE \/ Unit => B)(implicit - c: AppInterpreter[F], - semigroup: Semigroup[FAILURE] - ): F[B] = + def runReduce[B](appArgs: List[String] = Nil, f: FAILURE \/ Unit => B)(using AppCompiler[F], Semigroup[FAILURE]): F[B] = runMap[FAILURE \/ Unit](appArgs) .apply { case Left(failures) => Left(failures.reduce) @@ -557,11 +427,11 @@ sealed trait AppSyntax { } .map(f) - def runRaw(appArgs: List[String] = Nil)(implicit c: AppInterpreter[F]): F[NonEmptyList[FAILURE] \/ Unit] = + def runRaw(appArgs: List[String] = Nil)(using AppCompiler[F]): F[NonEmptyList[FAILURE] \/ Unit] = runMap[NonEmptyList[FAILURE] \/ Unit](appArgs)(identity) - def runMap[B](appArgs: List[String] = Nil)(f: NonEmptyList[FAILURE] \/ Unit => B)(implicit c: AppInterpreter[F]): F[B] = - c.run[B]( + def runMap[B](appArgs: List[String] = Nil)(f: NonEmptyList[FAILURE] \/ Unit => B)(using AppCompiler[F]): F[B] = + AppCompiler[F].run[B]( compile(appArgs).map { case Left(failure) => f(Left(NonEmptyList.one(failure))).pure[F] case Right(appLogic) => appLogic.map(f) @@ -569,14 +439,12 @@ sealed trait AppSyntax { ) } - implicit class AppThrowOps[F[+_]: Async: Parallel, APP_INFO <: SimpleAppInfo[?], LOGGER_T[ - _[_] - ]: LoggerAdapter, CONFIG: Show, RESOURCES, DEPENDENCIES]( + extension [F[+_]: Async: Parallel, APP_INFO <: SimpleAppInfo[?], LOGGER_T[_[_]]: LoggerAdapter, CONFIG: Show, RESOURCES, DEPENDENCIES]( app: App[F, Throwable, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] - ) { + )(using Throwable =:= Throwable) - def compile(appArgs: List[String] = Nil)(implicit c: AppInterpreter[F]): Resource[F, F[Unit]] = - c.compile(appArgs, app).flatMap { + def compile(appArgs: List[String] = Nil)(using AppCompiler[F]): Resource[F, F[Unit]] = + AppCompiler[F].compile(appArgs, app).flatMap { case Left(failure) => Resource.raiseError(failure) case Right(value) => @@ -587,10 +455,8 @@ sealed trait AppSyntax { }) } - def run_(implicit c: AppInterpreter[F]): F[Unit] = + def run_(implicit c: AppCompiler[F]): F[Unit] = run().void - def run(appArgs: List[String] = Nil)(implicit c: AppInterpreter[F]): F[ExitCode] = + def run(appArgs: List[String] = Nil)(implicit c: AppCompiler[F]): F[ExitCode] = c.run(compile(appArgs)).as(ExitCode.Success) - } -} 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 93% 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 741c154..384f205 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppInterpreter.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppCompiler.scala @@ -8,7 +8,7 @@ import cats.effect.{Async, Fiber, Ref, Resource} import com.geirolz.app.toolkit.FailureHandler.OnFailureBehaviour import com.geirolz.app.toolkit.logger.LoggerAdapter -trait AppInterpreter[F[+_]]: +trait AppCompiler[F[+_]]: def run[T](compiledApp: Resource[F, F[T]])(implicit F: MonadCancelThrow[F]): F[T] @@ -24,13 +24,13 @@ trait AppInterpreter[F[+_]]: P: Parallel[F] ): Resource[F, FAILURE \/ F[NonEmptyList[FAILURE] \/ Unit]] -object AppInterpreter: +object AppCompiler: import cats.syntax.all.* - def apply[F[+_]](implicit ac: AppInterpreter[F]): AppInterpreter[F] = ac + def apply[F[+_]](using ac: AppCompiler[F]): AppCompiler[F] = ac - given [F[+_]]: AppInterpreter[F] = new AppInterpreter[F] { + given [F[+_]]: AppCompiler[F] = new AppCompiler[F] { override def run[T](compiledApp: Resource[F, F[T]])(implicit F: MonadCancelThrow[F]): F[T] = compiledApp.useEval @@ -59,7 +59,7 @@ object AppInterpreter: otherResources <- EitherT.right[FAILURE](app.resourcesLoader) // group resources - appResources: App.Resources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] = App.Resources( + appResources: AppResources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] = AppResources( info = app.appInfo, args = AppArgs(appArgs), logger = userLogger, @@ -71,7 +71,7 @@ object AppInterpreter: _ <- toolkitResLogger.debug(app.appMessages.buildingServicesEnv) appDepServices <- EitherT(app.dependenciesLoader(appResources)) _ <- toolkitResLogger.info(app.appMessages.servicesEnvSuccessfullyBuilt) - appDependencies = App.Dependencies(appResources, appDepServices) + appDependencies = AppDependencies(appResources, appDepServices) // --------------------- SERVICES ------------------- _ <- toolkitResLogger.debug(app.appMessages.buildingApp) diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppDependencies.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppDependencies.scala new file mode 100644 index 0000000..9162086 --- /dev/null +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppDependencies.scala @@ -0,0 +1,48 @@ +package com.geirolz.app.toolkit + +import cats.syntax.all.given + +final case class AppDependencies[APP_INFO <: SimpleAppInfo[?], LOGGER, CONFIG, DEPENDENCIES, RESOURCES]( + private val _resources: AppResources[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"""AppDependencies( + | info = $info, + | args = $args, + | logger = $logger, + | config = $config, + | resources = $resources, + | dependencies = $dependencies + |)""".stripMargin + +object AppDependencies: + + private[toolkit] def apply[APP_INFO <: SimpleAppInfo[?], LOGGER, CONFIG, DEPENDENCIES, RESOURCES]( + resources: AppResources[APP_INFO, LOGGER, CONFIG, RESOURCES], + dependencies: DEPENDENCIES + ): AppDependencies[APP_INFO, LOGGER, CONFIG, DEPENDENCIES, RESOURCES] = + new AppDependencies[APP_INFO, LOGGER, CONFIG, DEPENDENCIES, RESOURCES]( + _resources = resources, + _dependencies = dependencies + ) + + def unapply[APP_INFO <: SimpleAppInfo[?], LOGGER, CONFIG, DEPENDENCIES, RESOURCES]( + deps: AppDependencies[APP_INFO, LOGGER, CONFIG, DEPENDENCIES, RESOURCES] + ): Option[(APP_INFO, AppArgs, LOGGER, CONFIG, RESOURCES, DEPENDENCIES)] = + ( + deps.info, + deps.args, + deps.logger, + deps.config, + deps.resources, + deps.dependencies + ).some diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppResources.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppResources.scala new file mode 100644 index 0000000..f008c31 --- /dev/null +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppResources.scala @@ -0,0 +1,52 @@ +package com.geirolz.app.toolkit + +import cats.syntax.all.given + +final case class AppResources[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"""AppDependencies( + | info = $info, + | args = $args, + | logger = $logger, + | config = $config, + | resources = $resources + |)""".stripMargin + +object AppResources: + + private[toolkit] def apply[APP_INFO <: SimpleAppInfo[?], LOGGER, CONFIG, RESOURCES]( + info: APP_INFO, + args: AppArgs, + logger: LOGGER, + config: CONFIG, + resources: RESOURCES + ): AppResources[APP_INFO, LOGGER, CONFIG, RESOURCES] = + new AppResources[APP_INFO, LOGGER, CONFIG, RESOURCES]( + info = info, + args = args, + logger = logger, + config = config, + resources = resources + ) + + def unapply[APP_INFO <: SimpleAppInfo[?], LOGGER, CONFIG, RESOURCES]( + res: AppResources[APP_INFO, LOGGER, CONFIG, RESOURCES] + ): Option[(APP_INFO, AppArgs, LOGGER, CONFIG, RESOURCES)] = + ( + res.info, + res.args, + res.logger, + res.config, + res.resources + ).some 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 db080ec..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/test/scala/com/geirolz/app/toolkit/AppResourcesAndDependenciesSuite.scala b/core/src/test/scala/com/geirolz/app/toolkit/AppResourcesAndDependenciesSuite.scala index 55fd4b8..33fa465 100644 --- a/core/src/test/scala/com/geirolz/app/toolkit/AppResourcesAndDependenciesSuite.scala +++ b/core/src/test/scala/com/geirolz/app/toolkit/AppResourcesAndDependenciesSuite.scala @@ -7,12 +7,12 @@ 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") { + test("AppResources unapply works as expected") { App[IO] .withInfo(TestAppInfo.value) .withLogger(ToolkitLogger.console[IO](_)) .withConfig(TestConfig.defaultTest) - .dependsOn { case _ | App.Resources(_, _, _, _, _) => + .dependsOn { case _ | AppResources(_, _, _, _, _) => Resource.eval(IO.unit) } .provideOne(_ => IO.unit) @@ -20,13 +20,13 @@ class AppResourcesAndDependenciesSuite extends munit.FunSuite { } // false positive not exhaustive pattern matching ? TODO: investigate - test("App.Dependencies unapply works as expected") { + test("AppDependencies unapply works as expected") { App[IO] .withInfo(TestAppInfo.value) .withLogger(ToolkitLogger.console[IO](_)) .withConfig(TestConfig.defaultTest) .withoutDependencies - .provideOne { case _ | App.Dependencies(_, _, _, _, _, _) => + .provideOne { case _ | AppDependencies(_, _, _, _, _, _) => IO.unit } .run_ diff --git a/docs/compiled/README.md b/docs/compiled/README.md index 62204c3..db23f59 100644 --- a/docs/compiled/README.md +++ b/docs/compiled/README.md @@ -85,7 +85,7 @@ object Config { case class AppDependencyServices(kafkaConsumer: KafkaConsumer[IO]) object AppDependencyServices { - def resource(res: App.Resources[SimpleAppInfo[String], ToolkitLogger[IO], Config, NoResources]): Resource[IO, AppDependencyServices] = + def resource(res: AppResources[SimpleAppInfo[String], ToolkitLogger[IO], Config, NoResources]): Resource[IO, AppDependencyServices] = Resource.pure(AppDependencyServices(KafkaConsumer.fake)) } diff --git a/docs/source/README.md b/docs/source/README.md index 5e28d44..d587244 100644 --- a/docs/source/README.md +++ b/docs/source/README.md @@ -85,7 +85,7 @@ object Config { case class AppDependencyServices(kafkaConsumer: KafkaConsumer[IO]) object AppDependencyServices { - def resource(res: App.Resources[SimpleAppInfo[String], ToolkitLogger[IO], Config, NoResources]): Resource[IO, AppDependencyServices] = + def resource(res: AppResources[SimpleAppInfo[String], ToolkitLogger[IO], Config, NoResources]): Resource[IO, AppDependencyServices] = Resource.pure(AppDependencyServices(KafkaConsumer.fake)) } 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 index e93aae8..4f7678e 100644 --- a/examples/src/main/scala-2/com/geirolz/example/app/AppDependencyServices.scala +++ b/examples/src/main/scala-2/com/geirolz/example/app/AppDependencyServices.scala @@ -10,7 +10,7 @@ case class AppDependencyServices( kafkaConsumer: KafkaConsumer[IO] ) object AppDependencyServices { - def resource(res: App.Resources[AppInfo, SelfAwareStructuredLogger[IO], AppConfig, NoResources]): Resource[IO, AppDependencyServices] = + def resource(res: AppResources[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/AppDependencyServices.scala b/examples/src/main/scala-3/com/geirolz/example/app/AppDependencyServices.scala index 4ae2774..56ccf26 100644 --- a/examples/src/main/scala-3/com/geirolz/example/app/AppDependencyServices.scala +++ b/examples/src/main/scala-3/com/geirolz/example/app/AppDependencyServices.scala @@ -11,7 +11,7 @@ case class AppDependencyServices( ) object AppDependencyServices: - def resource(res: App.Resources[AppInfo, SelfAwareStructuredLogger[IO], AppConfig, NoResources]): Resource[IO, AppDependencyServices] = + def resource(res: AppResources[AppInfo, SelfAwareStructuredLogger[IO], AppConfig, NoResources]): Resource[IO, AppDependencyServices] = Resource.pure( AppDependencyServices( KafkaConsumer.fake(res.config.kafkaBroker.host) 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 index 92d5896..bd6fceb 100644 --- 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 @@ -15,7 +15,7 @@ package object fly4s { 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] = + ): AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] = migrateDatabaseWith( url = d => url(d.config), user = d => user(d.config), @@ -25,12 +25,12 @@ package object fly4s { ) 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, + url: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => String, + user: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => Option[String] = (_: Any) => None, + password: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => Option[Array[Char]] = (_: Any) => None, + config: AppDependencies[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] = + ): AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] = migrateDatabase(dep => Fly4s .make[F]( @@ -43,8 +43,8 @@ package object fly4s { ) 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] = + f: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => Resource[F, Fly4s[F]] + ): AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] = dep => f(dep) .evalMap(fl4s => From 51f48a680ead49dfb9b651954dd52da942f52ef7 Mon Sep 17 00:00:00 2001 From: geirolz Date: Fri, 19 Jan 2024 12:24:04 +0100 Subject: [PATCH 04/22] Third iteration - missing syntax --- .../scala/com/geirolz/app/toolkit/App.scala | 337 +++--------------- .../com/geirolz/app/toolkit/AppBuilder.scala | 231 ++++++++++++ .../geirolz/app/toolkit/AppResources.scala | 3 +- .../scala/com/geirolz/app/toolkit/types.scala | 2 + 4 files changed, 281 insertions(+), 292 deletions(-) create mode 100644 core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala 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 fb4d60f..d9d2d7a 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/App.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/App.scala @@ -9,6 +9,7 @@ import com.geirolz.app.toolkit.logger.{LoggerAdapter, NoopLogger} import com.geirolz.app.toolkit.novalues.{NoConfig, NoDependencies, NoResources} import scala.util.NotGiven +import cats.syntax.all.given class App[ F[+_]: Async: Parallel, @@ -18,7 +19,7 @@ class App[ CONFIG: Show, RESOURCES, DEPENDENCIES -] private[App] ( +] private[toolkit] ( val appInfo: APP_INFO, val appMessages: AppMessages, val loggerBuilder: F[LOGGER_T[F]], @@ -30,33 +31,21 @@ class App[ val dependenciesLoader: AppResources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, FAILURE \/ DEPENDENCIES], val provideBuilder: AppDependencies[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 Resources = AppResources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] - - class Resourced[T] private (val appRes: Resources, 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: (Resources, T) = Resourced.unapply(this) - def useTupled[U](f: (Resources, T) => U): U = f.tupled(tupled) - def use[U](f: Resources => T => U): U = f(appRes)(value) - + type AppInfo = APP_INFO + type Logger = LOGGER_T[F] + type Config = CONFIG + type Self = App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] + private type SelfResources = AppResources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] + + class Resourced[T](val appRes: SelfResources, val value: T): + export appRes.* + def map[U](f: T => U): Resourced[U] = Resourced(appRes, f(value)) + def tupled: (SelfResources, T) = (appRes, value) + def useTupled[U](f: (SelfResources, T) => U): U = f.tupled(tupled) + def use[U](f: SelfResources => 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: Resources, value: T): Resourced[T] = new Resourced[T](appRes, value) - def unapply[T](r: Resourced[T]): (Resources, T) = (r.appRes, r.value) - def withMessages(messages: AppMessages): Self = copyWith(appMessages = messages) @@ -125,140 +114,17 @@ class App[ object App extends AppSyntax: - import cats.syntax.all.* - - def apply[F[+_]: Async: Parallel](using 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] - )(using 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) - - def withResources[RESOURCES2]( - resources: RESOURCES2 - ): AppBuilderSelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES2] = - withResourcesLoader(resources.pure[F]) + def apply[F[+_]: Async: Parallel](using DummyImplicit): AppBuilder[F, Throwable] = + AppBuilder[F, Throwable] - def withResourcesLoader[RESOURCES2]( - resourcesLoader: F[RESOURCES2] - ): AppBuilderSelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES2] = - withResourcesLoader(Resource.eval(resourcesLoader)) + def apply[F[+_]: Async: Parallel, FAILURE]: AppBuilder[F, FAILURE] = + AppBuilder[F, FAILURE] - def withResourcesLoader[RESOURCES2]( - resourcesLoader: Resource[F, RESOURCES2] - ): AppBuilderSelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES2] = - copyWith(resourcesLoader = resourcesLoader) - - // ------- DEPENDENCIES ------- - def withoutDependencies: AppBuilderSelectProvide[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, NoDependencies] = - dependsOn[NoDependencies, FAILURE](_ => Resource.pure(NoDependencies.value.asRight[FAILURE])) - - def dependsOn[DEPENDENCIES]( - f: AppResources[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: AppResources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, FAILURE2 \/ DEPENDENCIES] - )(using 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] - ) +sealed trait AppSyntax: - 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 - ) + import cats.syntax.all.* - final case class AppBuilderSelectProvide[ + extension [ F[+_]: Async: Parallel, FAILURE, APP_INFO <: SimpleAppInfo[?], @@ -266,126 +132,13 @@ object App extends AppSyntax: 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: AppResources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, FAILURE \/ DEPENDENCIES], - private val beforeProvidingF: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] - ): - - // ------- BEFORE PROVIDING ------- - def beforeProviding( - f: AppDependencies[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: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit], - fN: AppDependencies[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: AppDependencies[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: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Any] - )(using FAILURE =:= Throwable): App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = - provideOne[FAILURE](f.andThen(_.map(_.asRight[FAILURE]))) - - def provideOne[FAILURE2 <: FAILURE]( - f: AppDependencies[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: AppDependencies[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: AppDependencies[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: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => List[F[FAILURE2 \/ Any]] - )(using DummyImplicit): App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = - provideF[FAILURE2](f.andThen(_.pure[F])) - - // provideF - def provideF( - f: AppDependencies[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: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[List[F[FAILURE2 \/ Any]]] - )(using DummyImplicit): App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = - provideAttemptF(f.andThen(_.map(Right(_)))) - - def provideAttemptF[FAILURE2 <: FAILURE]( - f: AppDependencies[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: AppResources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, FAILURE \/ DEPENDENCIES], - beforeProvidingF: AppDependencies[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 - ) - -end App - -sealed trait AppSyntax: - - import cats.syntax.all.* - - extension [F[+_]: Async: Parallel, FAILURE, APP_INFO <: SimpleAppInfo[?], LOGGER_T[_[_]]: LoggerAdapter, CONFIG: Show, RESOURCES, DEPENDENCIES]( - app: App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] - )(using NotGiven[FAILURE =:= Throwable]) { + ](app: App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES]) + // -------------------- AppFailureSyntax -------------------- // failures def mapFailure[FAILURE2]( fhLoader: AppResources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => FailureHandler[F, FAILURE2] - )(f: FAILURE => FAILURE2): App[F, FAILURE2, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + )(f: FAILURE => FAILURE2)(using NotGiven[FAILURE =:= Throwable]): App[F, FAILURE2, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = app.copyWith[F, FAILURE2, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES]( failureHandlerLoader = fhLoader, dependenciesLoader = app.dependenciesLoader.andThen(_.map(_.leftMap(f))), @@ -394,32 +147,38 @@ sealed trait AppSyntax: def onFailure_( f: app.Resourced[FAILURE] => F[Unit] - ): App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + )(using NotGiven[FAILURE =:= Throwable]): 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] = + )(using NotGiven[FAILURE =:= Throwable]): 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] = + )(using NotGiven[FAILURE =:= Throwable]): 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)(using AppCompiler[F]): Resource[F, FAILURE \/ F[NonEmptyList[FAILURE] \/ Unit]] = + def compile( + appArgs: List[String] + )(using AppCompiler[F], NotGiven[FAILURE =:= Throwable]): Resource[F, FAILURE \/ F[NonEmptyList[FAILURE] \/ Unit]] = AppCompiler[F].compile(appArgs, app) - def run(appArgs: List[String] = Nil)(using AppCompiler[F]): F[ExitCode] = + def run(appArgs: List[String])(using AppCompiler[F], NotGiven[FAILURE =:= Throwable]): F[ExitCode] = runMap[ExitCode](appArgs).apply { case Left(_) => ExitCode.Error case Right(_) => ExitCode.Success } - def runReduce[B](appArgs: List[String] = Nil, f: FAILURE \/ Unit => B)(using AppCompiler[F], Semigroup[FAILURE]): F[B] = + def runReduce[B](appArgs: List[String], f: FAILURE \/ Unit => B)(using + AppCompiler[F], + Semigroup[FAILURE], + NotGiven[FAILURE =:= Throwable] + ): F[B] = runMap[FAILURE \/ Unit](appArgs) .apply { case Left(failures) => Left(failures.reduce) @@ -427,26 +186,22 @@ sealed trait AppSyntax: } .map(f) - def runRaw(appArgs: List[String] = Nil)(using AppCompiler[F]): F[NonEmptyList[FAILURE] \/ Unit] = + def runRaw(appArgs: List[String])(using AppCompiler[F], NotGiven[FAILURE =:= Throwable]): F[NonEmptyList[FAILURE] \/ Unit] = runMap[NonEmptyList[FAILURE] \/ Unit](appArgs)(identity) - def runMap[B](appArgs: List[String] = Nil)(f: NonEmptyList[FAILURE] \/ Unit => B)(using AppCompiler[F]): F[B] = + def runMap[B](appArgs: List[String])(f: NonEmptyList[FAILURE] \/ Unit => B)(using AppCompiler[F], NotGiven[FAILURE =:= Throwable]): F[B] = AppCompiler[F].run[B]( - compile(appArgs).map { + this.compile(appArgs).map { case Left(failure) => f(Left(NonEmptyList.one(failure))).pure[F] case Right(appLogic) => appLogic.map(f) } ) - } - - extension [F[+_]: Async: Parallel, APP_INFO <: SimpleAppInfo[?], LOGGER_T[_[_]]: LoggerAdapter, CONFIG: Show, RESOURCES, DEPENDENCIES]( - app: App[F, Throwable, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] - )(using Throwable =:= Throwable) - def compile(appArgs: List[String] = Nil)(using AppCompiler[F]): Resource[F, F[Unit]] = + // -------------------- AppThrowableSyntax -------------------- + def compile(appArgs: List[String])(using AppCompiler[F])(using env: FAILURE =:= Throwable): Resource[F, F[Unit]] = AppCompiler[F].compile(appArgs, app).flatMap { case Left(failure) => - Resource.raiseError(failure) + Resource.raiseError(env(failure)) case Right(value) => Resource.pure(value.flatMap { case Left(failures: NonEmptyList[Throwable]) => @@ -455,8 +210,8 @@ sealed trait AppSyntax: }) } - def run_(implicit c: AppCompiler[F]): F[Unit] = - run().void + def run_(using AppCompiler[F], FAILURE =:= Throwable): F[Unit] = + run().void - def run(appArgs: List[String] = Nil)(implicit c: AppCompiler[F]): F[ExitCode] = - c.run(compile(appArgs)).as(ExitCode.Success) + def run(appArgs: List[String] = Nil)(using AppCompiler[F], FAILURE =:= Throwable): F[ExitCode] = + AppCompiler[F].run(compile(appArgs)).as(ExitCode.Success) 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..78f82a4 --- /dev/null +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala @@ -0,0 +1,231 @@ +package com.geirolz.app.toolkit + +import cats.{Foldable, Parallel, Show} +import cats.effect.{Async, Resource} +import com.geirolz.app.toolkit.AppBuilder.SelectResAndDeps +import com.geirolz.app.toolkit.logger.{LoggerAdapter, NoopLogger} +import com.geirolz.app.toolkit.novalues.{NoConfig, NoDependencies, NoResources} + +import cats.syntax.all.given + +final class AppBuilder[F[+_]: Async: Parallel, FAILURE]: + def withInfo[APP_INFO <: SimpleAppInfo[?]]( + appInfo: APP_INFO + ): AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, NoopLogger, NoConfig, NoResources] = + new AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, NoopLogger, NoConfig, NoResources]( + appInfo = appInfo, + loggerBuilder = NoopLogger[F].pure[F], + configLoader = Resource.pure(NoConfig.value), + resourcesLoader = Resource.pure(NoResources.value) + ) + +object AppBuilder { + + def apply[F[+_]: Async: Parallel](using DummyImplicit): AppBuilder[F, Throwable] = + new AppBuilder[F, Throwable] + + def apply[F[+_]: Async: Parallel, FAILURE]: AppBuilder[F, FAILURE] = + new AppBuilder[F, FAILURE] + + final class SelectResAndDeps[ + F[+_]: Async: Parallel, + FAILURE, + APP_INFO <: SimpleAppInfo[?], + LOGGER_T[_[_]]: LoggerAdapter, + CONFIG: Show, + RESOURCES + ] private[AppBuilder] ( + appInfo: APP_INFO, + loggerBuilder: F[LOGGER_T[F]], + configLoader: Resource[F, CONFIG], + resourcesLoader: Resource[F, RESOURCES] + ): + + // ------- LOGGER ------- + def withNoopLogger: AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, NoopLogger, CONFIG, RESOURCES] = + withLogger(logger = NoopLogger[F]) + + def withLogger[LOGGER_T2[_[_]]: LoggerAdapter]( + logger: LOGGER_T2[F] + ): AppBuilder.SelectResAndDeps[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] + ): AppBuilder.SelectResAndDeps[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]] + ): AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T2, CONFIG, RESOURCES] = + copyWith(loggerBuilder = loggerBuilder(appInfo)) + + // ------- CONFIG ------- + def withoutConfig: AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, NoConfig, RESOURCES] = + withConfig[NoConfig](NoConfig.value) + + def withConfig[CONFIG2: Show]( + config: CONFIG2 + ): AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG2, RESOURCES] = + withConfigLoader(config.pure[F]) + + def withConfigLoader[CONFIG2: Show]( + configLoader: APP_INFO => F[CONFIG2] + )(using DummyImplicit): AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG2, RESOURCES] = + withConfigLoader(i => Resource.eval(configLoader(i))) + + def withConfigLoader[CONFIG2: Show]( + configLoader: F[CONFIG2] + ): AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG2, RESOURCES] = + withConfigLoader(Resource.eval(configLoader)) + + def withConfigLoader[CONFIG2: Show]( + configLoader: Resource[F, CONFIG2] + ): AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG2, RESOURCES] = + withConfigLoader(_ => configLoader) + + def withConfigLoader[CONFIG2: Show]( + configLoader: APP_INFO => Resource[F, CONFIG2] + ): AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG2, RESOURCES] = + copyWith(configLoader = configLoader(this.appInfo)) + + // ------- RESOURCES ------- + def withoutResources: AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, NoResources] = + withResources[NoResources](NoResources.value) + + def withResources[RESOURCES2]( + resources: RESOURCES2 + ): AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES2] = + withResourcesLoader(resources.pure[F]) + + def withResourcesLoader[RESOURCES2]( + resourcesLoader: F[RESOURCES2] + ): AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES2] = + withResourcesLoader(Resource.eval(resourcesLoader)) + + def withResourcesLoader[RESOURCES2]( + resourcesLoader: Resource[F, RESOURCES2] + ): AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES2] = + copyWith(resourcesLoader = resourcesLoader) + + // ------- DEPENDENCIES ------- + def withoutDependencies: AppBuilder.SelectProvide[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, NoDependencies] = + dependsOn[NoDependencies, FAILURE](_ => Resource.pure(NoDependencies.value.asRight[FAILURE])) + + def dependsOn[DEPENDENCIES]( + f: AppResources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, DEPENDENCIES] + ): AppBuilder.SelectProvide[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + dependsOn[DEPENDENCIES, FAILURE](f.andThen(_.map(_.asRight[FAILURE]))) + + def dependsOn[DEPENDENCIES, FAILURE2 <: FAILURE]( + f: AppResources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, FAILURE2 \/ DEPENDENCIES] + )(using DummyImplicit): AppBuilder.SelectProvide[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + AppBuilder.SelectProvide( + 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 AppBuilder.SelectResAndDeps[G, ERROR2, APP_INFO2, LOGGER_T2, CONFIG2, RESOURCES2]( + appInfo = appInfo, + loggerBuilder = loggerBuilder, + configLoader = configLoader, + resourcesLoader = resourcesLoader + ) + + final case class SelectProvide[ + F[+_]: Async: Parallel, + FAILURE, + APP_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: AppResources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, FAILURE \/ DEPENDENCIES], + private val beforeProvidingF: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] + ): + + // ------- BEFORE PROVIDING ------- + def beforeProviding( + f: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] + ): AppBuilder.SelectProvide[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + copy(beforeProvidingF = f) + + def beforeProvidingSeq( + f: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit], + fN: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit]* + ): AppBuilder.SelectProvide[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + beforeProvidingSeq(deps => (f +: fN).map(_(deps))) + + def beforeProvidingSeq[G[_]: Foldable]( + f: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => G[F[Unit]] + ): AppBuilder.SelectProvide[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + copy(beforeProvidingF = f(_).sequence_) + + // ------- PROVIDE ------- + def provideOne( + f: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Any] + )(using FAILURE =:= Throwable): App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + provideOne[FAILURE](f.andThen(_.map(_.asRight[FAILURE]))) + + def provideOne[FAILURE2 <: FAILURE]( + f: AppDependencies[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: AppDependencies[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: AppDependencies[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: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => List[F[FAILURE2 \/ Any]] + )(using DummyImplicit): App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + provideF[FAILURE2](f.andThen(_.pure[F])) + + // provideF + def provideF( + f: AppDependencies[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: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[List[F[FAILURE2 \/ Any]]] + )(using DummyImplicit): App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + provideAttemptF(f.andThen(_.map(Right(_)))) + + def provideAttemptF[FAILURE2 <: FAILURE]( + f: AppDependencies[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 + ) +} diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppResources.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppResources.scala index f008c31..653b4fc 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppResources.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppResources.scala @@ -8,7 +8,7 @@ final case class AppResources[APP_INFO <: SimpleAppInfo[?], LOGGER, CONFIG, RESO logger: LOGGER, config: CONFIG, resources: RESOURCES -): +) { type AppInfo = APP_INFO type Logger = LOGGER type Config = CONFIG @@ -22,6 +22,7 @@ final case class AppResources[APP_INFO <: SimpleAppInfo[?], LOGGER, CONFIG, RESO | config = $config, | resources = $resources |)""".stripMargin +} object AppResources: diff --git a/core/src/main/scala/com/geirolz/app/toolkit/types.scala b/core/src/main/scala/com/geirolz/app/toolkit/types.scala index 57c54b5..1113d46 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/types.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/types.scala @@ -4,3 +4,5 @@ import scala.annotation.targetName @targetName("Either") type \/[+A, +B] = Either[A, B] + + From 2bda9b0ce930db772aa9d894256cee4ae7cefa33 Mon Sep 17 00:00:00 2001 From: geirolz Date: Tue, 23 Jan 2024 17:02:02 +0100 Subject: [PATCH 05/22] IT COMPILES --- .../scala/com/geirolz/app/toolkit/App.scala | 235 ++++++-------- .../com/geirolz/app/toolkit/AppBuilder.scala | 299 ++++++++++-------- .../com/geirolz/app/toolkit/AppCompiler.scala | 45 ++- .../geirolz/app/toolkit/AppDependencies.scala | 26 +- .../app/toolkit/AppLogicTypeInterpreter.scala | 44 +++ .../com/geirolz/app/toolkit/AppMessages.scala | 10 +- .../geirolz/app/toolkit/AppResources.scala | 26 +- .../geirolz/app/toolkit/FailureHandler.scala | 12 +- .../app/toolkit/logger/ToolkitLogger.scala | 2 +- .../scala/com/geirolz/app/toolkit/types.scala | 6 +- .../AppResourcesAndDependenciesSuite.scala | 8 +- .../com/geirolz/app/toolkit/AppSuite.scala | 44 +-- docs/compiled/README.md | 4 +- docs/compiled/integrations.md | 4 +- docs/source/README.md | 4 +- docs/source/integrations.md | 4 +- examples/buildinfo.properties | 4 +- .../com/geirolz/example/app/AppConfig.scala | 39 --- .../example/app/AppDependencyServices.scala | 19 -- .../com/geirolz/example/app/AppInfo.scala | 39 --- .../com/geirolz/example/app/AppMain.scala | 34 -- .../example/app/provided/AppHttpServer.scala | 26 -- .../example/app/provided/KafkaConsumer.scala | 23 -- .../com/geirolz/example/app/AppConfig.scala | 0 .../example/app/AppDependencyServices.scala | 2 +- .../com/geirolz/example/app/AppError.scala | 3 +- .../com/geirolz/example/app/AppInfo.scala | 0 .../com/geirolz/example/app/AppMain.scala | 4 +- .../geirolz/example/app/AppWithFailures.scala | 10 +- .../example/app/provided/AppHttpServer.scala | 0 .../example/app/provided/KafkaConsumer.scala | 0 .../geirolz/app/toolkit/fly4s/package.scala | 22 +- .../app/toolkit/fly4s/Fly4sSupportSuite.scala | 2 +- .../logger/Log4CatsLoggerAdapterSuite.scala | 2 +- .../logger/OdinLoggerAdapterSuite.scala | 2 +- .../geirolz/app/toolkit/config/package.scala | 9 - .../config/PureconfigSecretSupportSuite.scala | 2 +- 37 files changed, 432 insertions(+), 583 deletions(-) create mode 100644 core/src/main/scala/com/geirolz/app/toolkit/AppLogicTypeInterpreter.scala delete mode 100644 examples/src/main/scala-2/com/geirolz/example/app/AppConfig.scala delete mode 100644 examples/src/main/scala-2/com/geirolz/example/app/AppDependencyServices.scala delete mode 100644 examples/src/main/scala-2/com/geirolz/example/app/AppInfo.scala delete mode 100644 examples/src/main/scala-2/com/geirolz/example/app/AppMain.scala delete mode 100644 examples/src/main/scala-2/com/geirolz/example/app/provided/AppHttpServer.scala delete mode 100644 examples/src/main/scala-2/com/geirolz/example/app/provided/KafkaConsumer.scala rename examples/src/main/{scala-3 => scala}/com/geirolz/example/app/AppConfig.scala (100%) rename examples/src/main/{scala-3 => scala}/com/geirolz/example/app/AppDependencyServices.scala (92%) rename examples/src/main/{scala-2 => scala}/com/geirolz/example/app/AppError.scala (85%) rename examples/src/main/{scala-3 => scala}/com/geirolz/example/app/AppInfo.scala (100%) rename examples/src/main/{scala-3 => scala}/com/geirolz/example/app/AppMain.scala (90%) rename examples/src/main/{scala-2 => scala}/com/geirolz/example/app/AppWithFailures.scala (79%) rename examples/src/main/{scala-3 => scala}/com/geirolz/example/app/provided/AppHttpServer.scala (100%) rename examples/src/main/{scala-3 => scala}/com/geirolz/example/app/provided/KafkaConsumer.scala (100%) delete mode 100644 integrations/pureconfig/src/main/scala/com/geirolz/app/toolkit/config/package.scala 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 d9d2d7a..ae90640 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/App.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/App.scala @@ -1,75 +1,116 @@ package com.geirolz.app.toolkit -import cats.data.NonEmptyList import cats.effect.* -import cats.{Endo, Foldable, Parallel, Semigroup, Show} +import cats.syntax.all.given +import cats.{Endo, Foldable, Parallel, 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 com.geirolz.app.toolkit.logger.LoggerAdapter +import scala.reflect.ClassTag import scala.util.NotGiven -import cats.syntax.all.given class App[ F[+_]: Async: Parallel, FAILURE, - APP_INFO <: SimpleAppInfo[?], + INFO <: SimpleAppInfo[?], LOGGER_T[_[_]]: LoggerAdapter, CONFIG: Show, RESOURCES, DEPENDENCIES ] private[toolkit] ( - val appInfo: APP_INFO, - val appMessages: AppMessages, + val info: INFO, + val messages: AppMessages, val loggerBuilder: F[LOGGER_T[F]], val configLoader: Resource[F, CONFIG], val resourcesLoader: Resource[F, RESOURCES], - val beforeProvidingF: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit], - val onFinalizeF: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit], - val failureHandlerLoader: AppResources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => FailureHandler[F, FAILURE], - val dependenciesLoader: AppResources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, FAILURE \/ DEPENDENCIES], - val provideBuilder: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[FAILURE \/ List[F[FAILURE \/ Any]]] + val beforeProvidingF: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit], + val onFinalizeF: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit], + val failureHandlerLoader: AppResources[INFO, LOGGER_T[F], CONFIG, RESOURCES] => FailureHandler[F, FAILURE], + val depsLoader: AppResources[INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, FAILURE \/ DEPENDENCIES], + val servicesBuilder: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[FAILURE \/ List[F[FAILURE \/ Unit]]] ): - type AppInfo = APP_INFO + type AppInfo = INFO type Logger = LOGGER_T[F] type Config = CONFIG - type Self = App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] - private type SelfResources = AppResources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] - - class Resourced[T](val appRes: SelfResources, val value: T): - export appRes.* - def map[U](f: T => U): Resourced[U] = Resourced(appRes, f(value)) - def tupled: (SelfResources, T) = (appRes, value) - def useTupled[U](f: (SelfResources, T) => U): U = f.tupled(tupled) - def use[U](f: SelfResources => 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) - - def withMessages(messages: AppMessages): Self = - copyWith(appMessages = messages) + type Self = App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] + private type SelfResources = AppResources[INFO, LOGGER_T[F], CONFIG, RESOURCES] + + class Resourced[T](val res: SelfResources, val value: T): + export res.* + def map[U](f: T => U): Resourced[U] = Resourced(res, f(value)) + def tupled: (SelfResources, T) = (res, value) + def useTupled[U](f: (SelfResources, T) => U): U = f.tupled(tupled) + def use[U](f: SelfResources => T => U): U = f(res)(value) + def tupledAll: (INFO, CONFIG, LOGGER_T[F], RESOURCES, T) = (info, config, logger, resources, value) + def useTupledAll[U](f: (INFO, CONFIG, LOGGER_T[F], RESOURCES, T) => U): U = f.tupled(tupledAll) def onFinalize( - f: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] + f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] ): Self = copyWith(onFinalizeF = f) def onFinalizeSeq( - f: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit], - fN: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit]* + f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit], + fN: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit]* ): Self = onFinalizeSeq(deps => (f +: fN).map(_(deps))) def onFinalizeSeq[G[_]: Foldable]( - f: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => G[F[Unit]] + f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => G[F[Unit]] ): Self = copyWith(onFinalizeF = f(_).sequence_) - private[toolkit] def _updateFailureHandlerLoader( - fh: AppResources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => Endo[FailureHandler[F, FAILURE]] + // -------------------- AppFailureSyntax -------------------- + // failures + def mapFailure[FAILURE2]( + fhLoader: AppResources[INFO, LOGGER_T[F], CONFIG, RESOURCES] => FailureHandler[F, FAILURE2] + )( + f: FAILURE => FAILURE2 + )(using NotNothing[FAILURE], NotNothing[FAILURE2]): App[F, FAILURE2, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + copyWith[F, FAILURE2, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES]( + failureHandlerLoader = fhLoader, + dependenciesLoader = depsLoader.andThen(_.map(_.leftMap(f))), + provideBuilder = servicesBuilder.andThen(_.map(_.leftMap(f).map(_.map(_.map(_.leftMap(f)))))) + ) + + def onFailure_( + f: Resourced[FAILURE] => F[Unit] + )(using NotNothing[FAILURE]): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + _updateFailureHandlerLoader(appRes => _.onFailure(failure => f(Resourced(appRes, failure)) >> failureHandlerLoader(appRes).onFailureF(failure))) + + def onFailure( + f: Resourced[FAILURE] => F[OnFailureBehaviour] + )(using NotNothing[FAILURE]): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + _updateFailureHandlerLoader(appRes => _.onFailure(f.compose(Resourced(appRes, _)))) + + def handleFailureWith( + f: Resourced[FAILURE] => F[FAILURE \/ Unit] + )(using NotNothing[FAILURE]): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + _updateFailureHandlerLoader(appRes => _.handleFailureWith(f.compose(Resourced(appRes, _)))) + + // compile and run + def compile(appArgs: List[String])(using c: AppCompiler[F], i: AppLogicTypeInterpreter[F, FAILURE]): Resource[F, F[i.Result[Unit]]] = + i.interpret(c.compile(appArgs, this)) + + def run_(appArgs: List[String])(using AppCompiler[F], AppLogicTypeInterpreter[F, FAILURE]): F[Unit] = + run(appArgs).void + + def run(appArgs: List[String])(using c: AppCompiler[F], i: AppLogicTypeInterpreter[F, FAILURE]): F[ExitCode] = + runRaw(appArgs) + .map(i.isSuccess(_)) + .ifF( + ifTrue = ExitCode.Success, + ifFalse = ExitCode.Error + ) + + def runRaw(appArgs: List[String])(using c: AppCompiler[F], i: AppLogicTypeInterpreter[F, FAILURE]): F[i.Result[Unit]] = + compile(appArgs).useEval + + private def _updateFailureHandlerLoader( + fh: AppResources[INFO, LOGGER_T[F], CONFIG, RESOURCES] => Endo[FailureHandler[F, FAILURE]] ): App[ F, FAILURE, - APP_INFO, + INFO, LOGGER_T, CONFIG, RESOURCES, @@ -79,7 +120,7 @@ class App[ failureHandlerLoader = appRes => fh(appRes)(failureHandlerLoader(appRes)) ) - private[toolkit] def copyWith[ + private def copyWith[ G[+_]: Async: Parallel, FAILURE2, APP_INFO2 <: SimpleAppInfo[?], @@ -88,130 +129,34 @@ class App[ RES2, DEPS2 ]( - appInfo: APP_INFO2 = this.appInfo, - appMessages: AppMessages = this.appMessages, + 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, beforeProvidingF: AppDependencies[APP_INFO2, LOGGER_T2[G], CONFIG2, DEPS2, RES2] => G[Unit] = this.beforeProvidingF, onFinalizeF: AppDependencies[APP_INFO2, LOGGER_T2[G], CONFIG2, DEPS2, RES2] => G[Unit] = this.onFinalizeF, failureHandlerLoader: AppResources[APP_INFO2, LOGGER_T2[G], CONFIG2, RES2] => FailureHandler[G, FAILURE2] = this.failureHandlerLoader, - dependenciesLoader: AppResources[APP_INFO2, LOGGER_T2[G], CONFIG2, RES2] => Resource[G, FAILURE2 \/ DEPS2] = this.dependenciesLoader, - provideBuilder: AppDependencies[APP_INFO2, LOGGER_T2[G], CONFIG2, DEPS2, RES2] => G[FAILURE2 \/ List[G[FAILURE2 \/ Any]]] = this.provideBuilder + dependenciesLoader: AppResources[APP_INFO2, LOGGER_T2[G], CONFIG2, RES2] => Resource[G, FAILURE2 \/ DEPS2] = this.depsLoader, + provideBuilder: AppDependencies[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, failureHandlerLoader = failureHandlerLoader, - dependenciesLoader = dependenciesLoader, - provideBuilder = provideBuilder + depsLoader = dependenciesLoader, + servicesBuilder = provideBuilder ) -object App extends AppSyntax: +object App: - def apply[F[+_]: Async: Parallel](using DummyImplicit): AppBuilder[F, Throwable] = - AppBuilder[F, Throwable] + def apply[F[+_]: Async: Parallel]: AppBuilder[F, Nothing] = + AppBuilder[F] - def apply[F[+_]: Async: Parallel, FAILURE]: AppBuilder[F, FAILURE] = + def apply[F[+_]: Async: Parallel, FAILURE: ClassTag]: AppBuilder[F, FAILURE] = AppBuilder[F, FAILURE] - -sealed trait AppSyntax: - - import cats.syntax.all.* - - extension [ - F[+_]: Async: Parallel, - FAILURE, - APP_INFO <: SimpleAppInfo[?], - LOGGER_T[_[_]]: LoggerAdapter, - CONFIG: Show, - RESOURCES, - DEPENDENCIES - ](app: App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES]) - - // -------------------- AppFailureSyntax -------------------- - // failures - def mapFailure[FAILURE2]( - fhLoader: AppResources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => FailureHandler[F, FAILURE2] - )(f: FAILURE => FAILURE2)(using NotGiven[FAILURE =:= Throwable]): App[F, FAILURE2, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = - app.copyWith[F, FAILURE2, APP_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)))))) - ) - - def onFailure_( - f: app.Resourced[FAILURE] => F[Unit] - )(using NotGiven[FAILURE =:= Throwable]): 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] - )(using NotGiven[FAILURE =:= Throwable]): 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] - )(using NotGiven[FAILURE =:= Throwable]): 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] - )(using AppCompiler[F], NotGiven[FAILURE =:= Throwable]): Resource[F, FAILURE \/ F[NonEmptyList[FAILURE] \/ Unit]] = - AppCompiler[F].compile(appArgs, app) - - def run(appArgs: List[String])(using AppCompiler[F], NotGiven[FAILURE =:= Throwable]): F[ExitCode] = - runMap[ExitCode](appArgs).apply { - case Left(_) => ExitCode.Error - case Right(_) => ExitCode.Success - } - - def runReduce[B](appArgs: List[String], f: FAILURE \/ Unit => B)(using - AppCompiler[F], - Semigroup[FAILURE], - NotGiven[FAILURE =:= Throwable] - ): F[B] = - runMap[FAILURE \/ Unit](appArgs) - .apply { - case Left(failures) => Left(failures.reduce) - case Right(_) => Right(()) - } - .map(f) - - def runRaw(appArgs: List[String])(using AppCompiler[F], NotGiven[FAILURE =:= Throwable]): F[NonEmptyList[FAILURE] \/ Unit] = - runMap[NonEmptyList[FAILURE] \/ Unit](appArgs)(identity) - - def runMap[B](appArgs: List[String])(f: NonEmptyList[FAILURE] \/ Unit => B)(using AppCompiler[F], NotGiven[FAILURE =:= Throwable]): F[B] = - AppCompiler[F].run[B]( - this.compile(appArgs).map { - case Left(failure) => f(Left(NonEmptyList.one(failure))).pure[F] - case Right(appLogic) => appLogic.map(f) - } - ) - - // -------------------- AppThrowableSyntax -------------------- - def compile(appArgs: List[String])(using AppCompiler[F])(using env: FAILURE =:= Throwable): Resource[F, F[Unit]] = - AppCompiler[F].compile(appArgs, app).flatMap { - case Left(failure) => - Resource.raiseError(env(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_(using AppCompiler[F], FAILURE =:= Throwable): F[Unit] = - run().void - - def run(appArgs: List[String] = Nil)(using AppCompiler[F], FAILURE =:= Throwable): F[ExitCode] = - AppCompiler[F].run(compile(appArgs)).as(ExitCode.Success) diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala index 78f82a4..e0f6bb2 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala @@ -5,123 +5,144 @@ import cats.effect.{Async, Resource} import com.geirolz.app.toolkit.AppBuilder.SelectResAndDeps import com.geirolz.app.toolkit.logger.{LoggerAdapter, NoopLogger} import com.geirolz.app.toolkit.novalues.{NoConfig, NoDependencies, NoResources} - import cats.syntax.all.given -final class AppBuilder[F[+_]: Async: Parallel, FAILURE]: - def withInfo[APP_INFO <: SimpleAppInfo[?]]( - appInfo: APP_INFO - ): AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, NoopLogger, NoConfig, NoResources] = - new AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, NoopLogger, NoConfig, NoResources]( - appInfo = appInfo, +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 { +object AppBuilder: - def apply[F[+_]: Async: Parallel](using DummyImplicit): AppBuilder[F, Throwable] = - new AppBuilder[F, Throwable] + def apply[F[+_]: Async: Parallel]: AppBuilder[F, Nothing] = + given ClassTag[Nothing] = ClassTag.Nothing + new AppBuilder[F, Nothing] - def apply[F[+_]: Async: Parallel, FAILURE]: AppBuilder[F, FAILURE] = + def apply[F[+_]: Async: Parallel, FAILURE: ClassTag: NotNothing]: AppBuilder[F, FAILURE] = new AppBuilder[F, FAILURE] final class SelectResAndDeps[ F[+_]: Async: Parallel, - FAILURE, - APP_INFO <: SimpleAppInfo[?], + FAILURE: ClassTag, + INFO <: SimpleAppInfo[?], LOGGER_T[_[_]]: LoggerAdapter, CONFIG: Show, RESOURCES ] private[AppBuilder] ( - appInfo: APP_INFO, + info: INFO, + messages: AppMessages, loggerBuilder: F[LOGGER_T[F]], configLoader: Resource[F, CONFIG], resourcesLoader: Resource[F, RESOURCES] ): + // ------- MESSAGES ------- + def withMessages(messages: AppMessages): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES] = + copyWith(messages = messages) + // ------- LOGGER ------- - def withNoopLogger: AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, NoopLogger, CONFIG, RESOURCES] = - withLogger(logger = NoopLogger[F]) + def withNoopLogger: AppBuilder.SelectResAndDeps[F, FAILURE, INFO, NoopLogger, CONFIG, RESOURCES] = + withPureLogger(logger = NoopLogger[F]) - def withLogger[LOGGER_T2[_[_]]: LoggerAdapter]( + def withPureLogger[LOGGER_T2[_[_]]: LoggerAdapter]( logger: LOGGER_T2[F] - ): AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T2, CONFIG, RESOURCES] = - withLogger[LOGGER_T2](loggerF = (_: APP_INFO) => logger) + ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T2, CONFIG, RESOURCES] = + withPureLogger[LOGGER_T2](f = (_: INFO) => logger) - def withLogger[LOGGER_T2[_[_]]: LoggerAdapter]( - loggerF: APP_INFO => LOGGER_T2[F] - ): AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T2, CONFIG, RESOURCES] = - withLoggerBuilder(loggerBuilder = appInfo => loggerF(appInfo).pure[F]) + def withPureLogger[LOGGER_T2[_[_]]: LoggerAdapter]( + f: INFO => LOGGER_T2[F] + ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T2, CONFIG, RESOURCES] = + withLoggerF(f = appInfo => f(appInfo).pure[F]) - def withLoggerBuilder[LOGGER_T2[_[_]]: LoggerAdapter]( - loggerBuilder: APP_INFO => F[LOGGER_T2[F]] - ): AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T2, CONFIG, RESOURCES] = - copyWith(loggerBuilder = loggerBuilder(appInfo)) + // TODO: Add failure + def withLoggerF[LOGGER_T2[_[_]]: LoggerAdapter]( + f: INFO => F[LOGGER_T2[F]] + ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T2, CONFIG, RESOURCES] = + copyWith(loggerBuilder = f(info)) // ------- CONFIG ------- - def withoutConfig: AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, NoConfig, RESOURCES] = - withConfig[NoConfig](NoConfig.value) + def withoutConfig: AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, NoConfig, RESOURCES] = + withPureConfig[NoConfig](NoConfig.value) - def withConfig[CONFIG2: Show]( + def withPureConfig[CONFIG2: Show]( config: CONFIG2 - ): AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG2, RESOURCES] = - withConfigLoader(config.pure[F]) + ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG2, RESOURCES] = + withConfigF(config.pure[F]) - def withConfigLoader[CONFIG2: Show]( - configLoader: APP_INFO => F[CONFIG2] - )(using DummyImplicit): AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG2, RESOURCES] = - withConfigLoader(i => Resource.eval(configLoader(i))) + // TODO: Add failure + 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))) - def withConfigLoader[CONFIG2: Show]( + // TODO: Add failure + def withConfigF[CONFIG2: Show]( configLoader: F[CONFIG2] - ): AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG2, RESOURCES] = - withConfigLoader(Resource.eval(configLoader)) + ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG2, RESOURCES] = + withConfig(Resource.eval(configLoader)) - def withConfigLoader[CONFIG2: Show]( + // TODO: Add failure + def withConfig[CONFIG2: Show]( configLoader: Resource[F, CONFIG2] - ): AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG2, RESOURCES] = - withConfigLoader(_ => configLoader) + ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG2, RESOURCES] = + withConfig(_ => configLoader) - def withConfigLoader[CONFIG2: Show]( - configLoader: APP_INFO => Resource[F, CONFIG2] - ): AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG2, RESOURCES] = - copyWith(configLoader = configLoader(this.appInfo)) + // TODO: Add failure + def withConfig[CONFIG2: Show]( + configLoader: INFO => Resource[F, CONFIG2] + ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG2, RESOURCES] = + copyWith(configLoader = configLoader(this.info)) // ------- RESOURCES ------- - def withoutResources: AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, NoResources] = - withResources[NoResources](NoResources.value) + def withoutResources: AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG, NoResources] = + withPureResources[NoResources](NoResources.value) - def withResources[RESOURCES2]( + def withPureResources[RESOURCES2]( resources: RESOURCES2 - ): AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES2] = - withResourcesLoader(resources.pure[F]) + ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES2] = + withResourcesF(resources.pure[F]) - def withResourcesLoader[RESOURCES2]( + // TODO: Add failure + def withResourcesF[RESOURCES2]( resourcesLoader: F[RESOURCES2] - ): AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES2] = - withResourcesLoader(Resource.eval(resourcesLoader)) + ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES2] = + withResources(Resource.eval(resourcesLoader)) - def withResourcesLoader[RESOURCES2]( + // TODO: Add failure + def withResources[RESOURCES2]( resourcesLoader: Resource[F, RESOURCES2] - ): AppBuilder.SelectResAndDeps[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES2] = + ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES2] = copyWith(resourcesLoader = resourcesLoader) // ------- DEPENDENCIES ------- - def withoutDependencies: AppBuilder.SelectProvide[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, NoDependencies] = - dependsOn[NoDependencies, FAILURE](_ => Resource.pure(NoDependencies.value.asRight[FAILURE])) - - def dependsOn[DEPENDENCIES]( - f: AppResources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, DEPENDENCIES] - ): AppBuilder.SelectProvide[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = - dependsOn[DEPENDENCIES, FAILURE](f.andThen(_.map(_.asRight[FAILURE]))) - - def dependsOn[DEPENDENCIES, FAILURE2 <: FAILURE]( - f: AppResources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, FAILURE2 \/ DEPENDENCIES] - )(using DummyImplicit): AppBuilder.SelectProvide[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + def withoutDependencies: AppBuilder.SelectProvide[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, NoDependencies] = + dependsOn[NoDependencies, FAILURE](_ => Resource.pure(NoDependencies.value)) + + // TODO: Check ClassTag + def dependsOn[DEPENDENCIES: ClassTag, FAILURE2 <: FAILURE: ClassTag]( + f: AppResources[INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, FAILURE2 | DEPENDENCIES] + ): AppBuilder.SelectProvide[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + dependsOnE[DEPENDENCIES, FAILURE2](f.andThen(_.map { + case deps: DEPENDENCIES => Right(deps) + case failure: FAILURE2 => Left(failure) + })) + + def dependsOnE[DEPENDENCIES, FAILURE2 <: FAILURE]( + f: AppResources[INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, FAILURE2 \/ DEPENDENCIES] + )(using DummyImplicit): AppBuilder.SelectProvide[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = AppBuilder.SelectProvide( - appInfo = appInfo, + info = info, + messages = messages, loggerBuilder = loggerBuilder, configLoader = configLoader, resourcesLoader = resourcesLoader, @@ -129,13 +150,17 @@ object AppBuilder { beforeProvidingF = _ => ().pure[F] ) - private def copyWith[G[+_]: Async: Parallel, ERROR2, APP_INFO2 <: SimpleAppInfo[?], LOGGER_T2[_[_]]: LoggerAdapter, CONFIG2: Show, RESOURCES2]( - appInfo: APP_INFO2 = this.appInfo, + 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, ERROR2, APP_INFO2, LOGGER_T2, CONFIG2, RESOURCES2]( - appInfo = appInfo, + ) = new AppBuilder.SelectResAndDeps[G, FAILURE2, INFO2, LOGGER_T2, CONFIG2, RESOURCES2]( + info = info, + messages = messages, loggerBuilder = loggerBuilder, configLoader = configLoader, resourcesLoader = resourcesLoader @@ -144,88 +169,106 @@ object AppBuilder { final case class SelectProvide[ F[+_]: Async: Parallel, FAILURE, - APP_INFO <: SimpleAppInfo[?], + INFO <: SimpleAppInfo[?], LOGGER_T[_[_]]: LoggerAdapter, CONFIG: Show, RESOURCES, DEPENDENCIES ]( - private val appInfo: APP_INFO, + private val info: INFO, + private val messages: AppMessages, private val loggerBuilder: F[LOGGER_T[F]], private val configLoader: Resource[F, CONFIG], private val resourcesLoader: Resource[F, RESOURCES], - private val dependenciesLoader: AppResources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, FAILURE \/ DEPENDENCIES], - private val beforeProvidingF: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] + private val dependenciesLoader: AppResources[INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, FAILURE \/ DEPENDENCIES], + private val beforeProvidingF: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] ): // ------- BEFORE PROVIDING ------- + // TODO: Add failure def beforeProviding( - f: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] - ): AppBuilder.SelectProvide[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] + ): AppBuilder.SelectProvide[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = copy(beforeProvidingF = f) + // TODO: Add failure def beforeProvidingSeq( - f: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit], - fN: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit]* - ): AppBuilder.SelectProvide[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit], + fN: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit]* + ): AppBuilder.SelectProvide[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = beforeProvidingSeq(deps => (f +: fN).map(_(deps))) + // TODO: Add failure def beforeProvidingSeq[G[_]: Foldable]( - f: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => G[F[Unit]] - ): AppBuilder.SelectProvide[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => G[F[Unit]] + ): AppBuilder.SelectProvide[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = copy(beforeProvidingF = f(_).sequence_) // ------- PROVIDE ------- - def provideOne( - f: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Any] - )(using FAILURE =:= Throwable): App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = - provideOne[FAILURE](f.andThen(_.map(_.asRight[FAILURE]))) - - def provideOne[FAILURE2 <: FAILURE]( - f: AppDependencies[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 provideOne[FAILURE2 <: FAILURE: ClassTag]( + f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[FAILURE2 | Unit] + ): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + provideOneE[FAILURE2](f.andThen(_.map { + case failure: FAILURE2 => Left(failure) + case _: Unit => Right(()) + })) + + def provideOneE[FAILURE2 <: FAILURE]( + f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[FAILURE2 \/ Unit] + ): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + provideE[FAILURE2](f.andThen(List(_))) def provideOneF[FAILURE2 <: FAILURE]( - f: AppDependencies[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])))))) + f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[FAILURE2 \/ F[Unit]] + ): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + provideAttemptFE[FAILURE2](f.andThen(_.map(_.map(v => List(v.map(_.asRight[FAILURE2])))))) // provide - def provide( - f: AppDependencies[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: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => List[F[FAILURE2 \/ Any]] - )(using DummyImplicit): App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = - provideF[FAILURE2](f.andThen(_.pure[F])) + def provide[FAILURE2 <: FAILURE: ClassTag]( + f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => List[F[FAILURE2 | Unit]] + ): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + provideE(f.andThen(_.map(_.map { + case failure: FAILURE2 => Left(failure) + case _: Unit => Right(()) + }))) + + def provideE[FAILURE2 <: FAILURE]( + f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => List[F[FAILURE2 \/ Unit]] + )(using DummyImplicit): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + provideFE[FAILURE2](f.andThen(_.pure[F])) // provideF - def provideF( - f: AppDependencies[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: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[List[F[FAILURE2 \/ Any]]] - )(using DummyImplicit): App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = - provideAttemptF(f.andThen(_.map(Right(_)))) - - def provideAttemptF[FAILURE2 <: FAILURE]( - f: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[FAILURE2 \/ List[F[FAILURE2 \/ Any]]] - ): App[F, FAILURE, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + def provideF[FAILURE2 <: FAILURE: ClassTag]( + f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[List[F[FAILURE2 | Unit]]] + )(using DummyImplicit): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + provideFE(f.andThen(_.map(_.map(_.map { + case failure: FAILURE2 => Left(failure) + case _: Unit => Right(()) + })))) + + def provideFE[FAILURE2 <: FAILURE]( + f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[List[F[FAILURE2 \/ Unit]]] + )(using DummyImplicit): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + provideAttemptFE(f.andThen(_.map(Right(_)))) + + // TODO Missing the union version + def provideAttemptFE[FAILURE2 <: FAILURE]( + f: AppDependencies[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( - appInfo = appInfo, - appMessages = AppMessages.default(appInfo), - failureHandlerLoader = _ => FailureHandler.cancelAll, - loggerBuilder = loggerBuilder, - resourcesLoader = resourcesLoader, - beforeProvidingF = beforeProvidingF, - onFinalizeF = _ => ().pure[F], - configLoader = configLoader, - dependenciesLoader = dependenciesLoader, - provideBuilder = f + info = info, + messages = messages, + failureHandlerLoader = res => + FailureHandler.logAndCancelAll[F, FAILURE]( + appMessages = res.messages, + logger = LoggerAdapter[LOGGER_T].toToolkit(res.logger) + ), + loggerBuilder = loggerBuilder, + resourcesLoader = resourcesLoader, + beforeProvidingF = beforeProvidingF, + onFinalizeF = _ => ().pure[F], + configLoader = configLoader, + depsLoader = dependenciesLoader, + servicesBuilder = f ) -} diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppCompiler.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppCompiler.scala index 384f205..338d179 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppCompiler.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppCompiler.scala @@ -1,25 +1,23 @@ 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 cats.{Parallel, Show} import com.geirolz.app.toolkit.FailureHandler.OnFailureBehaviour import com.geirolz.app.toolkit.logger.LoggerAdapter trait AppCompiler[F[+_]]: - def run[T](compiledApp: Resource[F, F[T]])(implicit F: MonadCancelThrow[F]): F[T] - 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]] @@ -32,12 +30,10 @@ object AppCompiler: given [F[+_]]: AppCompiler[F] = new AppCompiler[F] { - override def run[T](compiledApp: Resource[F, F[T]])(implicit F: MonadCancelThrow[F]): F[T] = compiledApp.useEval - - 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 { @@ -50,17 +46,18 @@ object AppCompiler: ) // 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: AppResources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] = AppResources( - info = app.appInfo, + appResources: AppResources[INFO, LOGGER_T[F], CONFIG, RESOURCES] = AppResources( + info = app.info, + messages = app.messages, args = AppArgs(appArgs), logger = userLogger, config = appConfig, @@ -68,15 +65,15 @@ object AppCompiler: ) // ------------------- DEPENDENCIES ----------------- - _ <- toolkitResLogger.debug(app.appMessages.buildingServicesEnv) - appDepServices <- EitherT(app.dependenciesLoader(appResources)) - _ <- toolkitResLogger.info(app.appMessages.servicesEnvSuccessfullyBuilt) + _ <- toolkitResLogger.debug(app.messages.buildingServicesEnv) + appDepServices <- EitherT(app.depsLoader(appResources)) + _ <- toolkitResLogger.info(app.messages.servicesEnvSuccessfullyBuilt) appDependencies = AppDependencies(appResources, 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(appDependencies))) + _ <- toolkitResLogger.info(app.messages.appSuccessfullyBuilt) // --------------------- APP ------------------------ appLogic = for { @@ -112,13 +109,13 @@ object AppCompiler: maybeReducedFailures <- failures.get.map(NonEmptyList.fromList(_)) } yield maybeReducedFailures.toLeft(()) } yield { - toolkitLogger.info(app.appMessages.startingApp) >> + toolkitLogger.info(app.messages.startingApp) >> app.beforeProvidingF(appDependencies) >> 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.onFinalizeF(appDependencies) >> toolkitLogger.info(app.messages.shuttingDownApp) ) } ).value diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppDependencies.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppDependencies.scala index 9162086..6c3c5be 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppDependencies.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppDependencies.scala @@ -2,16 +2,12 @@ package com.geirolz.app.toolkit import cats.syntax.all.given -final case class AppDependencies[APP_INFO <: SimpleAppInfo[?], LOGGER, CONFIG, DEPENDENCIES, RESOURCES]( - private val _resources: AppResources[APP_INFO, LOGGER, CONFIG, RESOURCES], +final case class AppDependencies[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, DEPENDENCIES, RESOURCES]( + private val _resources: AppResources[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 + + export _resources.{args, config, info, logger, resources} val dependencies: DEPENDENCIES = _dependencies override def toString: String = @@ -26,18 +22,18 @@ final case class AppDependencies[APP_INFO <: SimpleAppInfo[?], LOGGER, CONFIG, D object AppDependencies: - private[toolkit] def apply[APP_INFO <: SimpleAppInfo[?], LOGGER, CONFIG, DEPENDENCIES, RESOURCES]( - resources: AppResources[APP_INFO, LOGGER, CONFIG, RESOURCES], + private[toolkit] def apply[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, DEPENDENCIES, RESOURCES]( + resources: AppResources[INFO, LOGGER, CONFIG, RESOURCES], dependencies: DEPENDENCIES - ): AppDependencies[APP_INFO, LOGGER, CONFIG, DEPENDENCIES, RESOURCES] = - new AppDependencies[APP_INFO, LOGGER, CONFIG, DEPENDENCIES, RESOURCES]( + ): AppDependencies[INFO, LOGGER, CONFIG, DEPENDENCIES, RESOURCES] = + new AppDependencies[INFO, LOGGER, CONFIG, DEPENDENCIES, RESOURCES]( _resources = resources, _dependencies = dependencies ) - def unapply[APP_INFO <: SimpleAppInfo[?], LOGGER, CONFIG, DEPENDENCIES, RESOURCES]( - deps: AppDependencies[APP_INFO, LOGGER, CONFIG, DEPENDENCIES, RESOURCES] - ): Option[(APP_INFO, AppArgs, LOGGER, CONFIG, RESOURCES, DEPENDENCIES)] = + def unapply[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, DEPENDENCIES, RESOURCES]( + deps: AppDependencies[INFO, LOGGER, CONFIG, DEPENDENCIES, RESOURCES] + ): Option[(INFO, AppArgs, LOGGER, CONFIG, RESOURCES, DEPENDENCIES)] = ( deps.info, deps.args, diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppLogicTypeInterpreter.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppLogicTypeInterpreter.scala new file mode 100644 index 0000000..f6f6547 --- /dev/null +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppLogicTypeInterpreter.scala @@ -0,0 +1,44 @@ +package com.geirolz.app.toolkit + +import cats.{Applicative, Functor, MonadThrow} +import cats.data.NonEmptyList +import cats.effect.Resource + +trait AppLogicTypeInterpreter[F[_], FAILURE]: + type Result[T] + def interpret[T](appLogic: Resource[F, FAILURE \/ F[NonEmptyList[FAILURE] \/ T]]): Resource[F, F[Result[T]]] + def isSuccess[T](value: Result[T]): Boolean + +object AppLogicTypeInterpreter: + + import cats.syntax.all.* + + def apply[F[_], FAILURE](using i: AppLogicTypeInterpreter[F, FAILURE]): AppLogicTypeInterpreter[F, FAILURE] = i + + given [F[_]: Applicative, FAILURE: NotNothing]: AppLogicTypeInterpreter[F, FAILURE] = + new AppLogicTypeInterpreter[F, FAILURE]: + override type Result[T] = NonEmptyList[FAILURE] \/ T + 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 + } + + given [F[_]: MonadThrow]: AppLogicTypeInterpreter[F, Nothing] = + new AppLogicTypeInterpreter[F, Nothing]: + override type Result[T] = T + override def isSuccess[T](value: T): Boolean = true + override def interpret[T](appLogic: Resource[F, Nothing \/ F[NonEmptyList[Nothing] \/ T]]): Resource[F, F[T]] = + appLogic.map { + case Left(_) => + MonadThrow[F].raiseError(new RuntimeException("Unreachable point.")) + case Right(value: F[NonEmptyList[Nothing] \/ T]) => + value.flatMap { + case Left(_) => + MonadThrow[F].raiseError(new RuntimeException("Unreachable point.")) + case Right(value) => + value.pure[F] + } + } 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 faee952..53d06be 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppMessages.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppMessages.scala @@ -9,13 +9,14 @@ 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 + def fromAppInfo[INFO <: SimpleAppInfo[?]](info: INFO)( + f: INFO => AppMessages ): AppMessages = f(info) def default(info: SimpleAppInfo[?]): AppMessages = @@ -29,7 +30,8 @@ object AppMessages: appSuccessfullyBuilt = "App successfully built.", startingApp = s"Starting ${info.buildRefName}...", appWasStopped = s"${info.name} was stopped.", - appEnErrorOccurred = s"${info.name} was stopped due an error.", + 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/AppResources.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppResources.scala index 653b4fc..e30b658 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppResources.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppResources.scala @@ -2,20 +2,21 @@ package com.geirolz.app.toolkit import cats.syntax.all.given -final case class AppResources[APP_INFO <: SimpleAppInfo[?], LOGGER, CONFIG, RESOURCES]( - info: APP_INFO, +final case class AppResources[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, RESOURCES]( + info: INFO, + messages: AppMessages, args: AppArgs, logger: LOGGER, config: CONFIG, resources: RESOURCES ) { - type AppInfo = APP_INFO + type AppInfo = INFO type Logger = LOGGER type Config = CONFIG type Resources = RESOURCES override def toString: String = - s"""AppDependencies( + s"""AppResources( | info = $info, | args = $args, | logger = $logger, @@ -26,26 +27,29 @@ final case class AppResources[APP_INFO <: SimpleAppInfo[?], LOGGER, CONFIG, RESO object AppResources: - private[toolkit] def apply[APP_INFO <: SimpleAppInfo[?], LOGGER, CONFIG, RESOURCES]( - info: APP_INFO, + private[toolkit] def apply[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, RESOURCES]( + info: INFO, + messages: AppMessages, args: AppArgs, logger: LOGGER, config: CONFIG, resources: RESOURCES - ): AppResources[APP_INFO, LOGGER, CONFIG, RESOURCES] = - new AppResources[APP_INFO, LOGGER, CONFIG, RESOURCES]( + ): AppResources[INFO, LOGGER, CONFIG, RESOURCES] = + new AppResources[INFO, LOGGER, CONFIG, RESOURCES]( info = info, + messages = messages, args = args, logger = logger, config = config, resources = resources ) - def unapply[APP_INFO <: SimpleAppInfo[?], LOGGER, CONFIG, RESOURCES]( - res: AppResources[APP_INFO, LOGGER, CONFIG, RESOURCES] - ): Option[(APP_INFO, AppArgs, LOGGER, CONFIG, RESOURCES)] = + def unapply[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, RESOURCES]( + res: AppResources[INFO, LOGGER, CONFIG, RESOURCES] + ): Option[(INFO, AppMessages, AppArgs, LOGGER, CONFIG, RESOURCES)] = ( res.info, + res.messages, res.args, res.logger, res.config, diff --git a/core/src/main/scala/com/geirolz/app/toolkit/FailureHandler.scala b/core/src/main/scala/com/geirolz/app/toolkit/FailureHandler.scala index ce2ee39..d9ff9e8 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/FailureHandler.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/FailureHandler.scala @@ -1,8 +1,10 @@ package com.geirolz.app.toolkit -import cats.{~>, Applicative, Functor} +import cats.{~>, Applicative, Functor, Monad, Show} import cats.data.NonEmptyList import com.geirolz.app.toolkit.FailureHandler.OnFailureBehaviour +import com.geirolz.app.toolkit.logger.ToolkitLogger +import cats.syntax.all.* case class FailureHandler[F[_], FAILURE]( onFailureF: FAILURE => F[OnFailureBehaviour], @@ -33,9 +35,15 @@ object FailureHandler extends FailureHandlerSyntax: def summon[F[_], E](implicit ev: FailureHandler[F, E]): FailureHandler[F, E] = ev + def logAndCancelAll[F[_]: Monad, FAILURE](appMessages: AppMessages, logger: ToolkitLogger[F]): FailureHandler[F, FAILURE] = + doNothing[F, FAILURE]().onFailure(failure => logger.error(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.CancelAll), + onFailureF = (_: FAILURE) => Applicative[F].pure(OnFailureBehaviour.DoNothing), handleFailureWithF = (e: FAILURE) => Applicative[F].pure(Left(e)) ) 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 index f99e3c4..399a31b 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/logger/ToolkitLogger.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/logger/ToolkitLogger.scala @@ -41,7 +41,7 @@ object ToolkitLogger: case Level.Warn => "WARN" case Level.Info => "INFO" case Level.Debug => "DEBUG" - case Level.Trace => "Trace" + case Level.Trace => "TRACE" object Level: case object Error extends Level diff --git a/core/src/main/scala/com/geirolz/app/toolkit/types.scala b/core/src/main/scala/com/geirolz/app/toolkit/types.scala index 1113d46..d11e80f 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/types.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/types.scala @@ -1,8 +1,8 @@ package com.geirolz.app.toolkit import scala.annotation.targetName +import scala.util.NotGiven @targetName("Either") -type \/[+A, +B] = Either[A, B] - - +type \/[+A, +B] = Either[A, B] +type NotNothing[T] = NotGiven[T =:= Nothing] \ No newline at end of file diff --git a/core/src/test/scala/com/geirolz/app/toolkit/AppResourcesAndDependenciesSuite.scala b/core/src/test/scala/com/geirolz/app/toolkit/AppResourcesAndDependenciesSuite.scala index 33fa465..a39f3d7 100644 --- a/core/src/test/scala/com/geirolz/app/toolkit/AppResourcesAndDependenciesSuite.scala +++ b/core/src/test/scala/com/geirolz/app/toolkit/AppResourcesAndDependenciesSuite.scala @@ -10,8 +10,8 @@ class AppResourcesAndDependenciesSuite extends munit.FunSuite { test("AppResources unapply works as expected") { App[IO] .withInfo(TestAppInfo.value) - .withLogger(ToolkitLogger.console[IO](_)) - .withConfig(TestConfig.defaultTest) + .withPureLogger(ToolkitLogger.console[IO](_)) + .withPureConfig(TestConfig.defaultTest) .dependsOn { case _ | AppResources(_, _, _, _, _) => Resource.eval(IO.unit) } @@ -23,8 +23,8 @@ class AppResourcesAndDependenciesSuite extends munit.FunSuite { test("AppDependencies unapply works as expected") { App[IO] .withInfo(TestAppInfo.value) - .withLogger(ToolkitLogger.console[IO](_)) - .withConfig(TestConfig.defaultTest) + .withPureLogger(ToolkitLogger.console[IO](_)) + .withPureConfig(TestConfig.defaultTest) .withoutDependencies .provideOne { case _ | AppDependencies(_, _, _, _, _, _) => IO.unit 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..deb558c 100644 --- a/core/src/test/scala/com/geirolz/app/toolkit/AppSuite.scala +++ b/core/src/test/scala/com/geirolz/app/toolkit/AppSuite.scala @@ -22,8 +22,8 @@ class AppSuite extends munit.CatsEffectSuite { counter: Ref[IO, Int] <- IO.ref(0) _ <- App[IO] .withInfo(TestAppInfo.value) - .withLogger(ToolkitLogger.console[IO](_)) - .withConfig(TestConfig.defaultTest) + .withPureLogger(ToolkitLogger.console[IO](_)) + .withPureConfig(TestConfig.defaultTest) .dependsOn(_ => Resource.pure[IO, Ref[IO, Int]](counter).trace(LabeledResource.appDependencies)) .provideOne(_.dependencies.set(1)) .compile() @@ -65,8 +65,8 @@ class AppSuite extends munit.CatsEffectSuite { for { _ <- App[IO] .withInfo(TestAppInfo.value) - .withLogger(ToolkitLogger.console[IO](_)) - .withConfig(TestConfig.defaultTest) + .withPureLogger(ToolkitLogger.console[IO](_)) + .withPureConfig(TestConfig.defaultTest) .dependsOn(_ => Resource.pure[IO, Unit](()).trace(LabeledResource.appDependencies)) .provideOneF(_ => IO.raiseError(ex"BOOM!")) .compile() @@ -101,8 +101,8 @@ class AppSuite extends munit.CatsEffectSuite { for { _ <- App[IO] .withInfo(TestAppInfo.value) - .withLogger(ToolkitLogger.console[IO](_)) - .withConfig(TestConfig.defaultTest) + .withPureLogger(ToolkitLogger.console[IO](_)) + .withPureConfig(TestConfig.defaultTest) .withoutDependencies .provide(_ => List( @@ -143,8 +143,8 @@ class AppSuite extends munit.CatsEffectSuite { for { _ <- App[IO] .withInfo(TestAppInfo.value) - .withLogger(ToolkitLogger.console[IO](_)) - .withConfig(TestConfig.defaultTest) + .withPureLogger(ToolkitLogger.console[IO](_)) + .withPureConfig(TestConfig.defaultTest) .withoutDependencies .provideOne(_ => IO.sleep(1.second)) .compile() @@ -179,8 +179,8 @@ class AppSuite extends munit.CatsEffectSuite { for { _ <- App[IO] .withInfo(TestAppInfo.value) - .withLogger(ToolkitLogger.console[IO](_)) - .withConfig(TestConfig.defaultTest) + .withPureLogger(ToolkitLogger.console[IO](_)) + .withPureConfig(TestConfig.defaultTest) .withoutDependencies .provideF(_ => IO( @@ -225,8 +225,8 @@ class AppSuite extends munit.CatsEffectSuite { _ <- App[IO] .withInfo(TestAppInfo.value) - .withLogger(ToolkitLogger.console[IO](_)) - .withConfig(TestConfig.defaultTest) + .withPureLogger(ToolkitLogger.console[IO](_)) + .withPureConfig(TestConfig.defaultTest) .dependsOn(_ => Resource.pure[IO, Unit](()).trace(LabeledResource.appDependencies)) .provideOne(_ => IO.raiseError(ex"BOOM!")) .compile() @@ -265,8 +265,8 @@ class AppSuite extends munit.CatsEffectSuite { for { _ <- App[IO] .withInfo(TestAppInfo.value) - .withLogger(ToolkitLogger.console[IO](_)) - .withConfig(TestConfig.defaultTest) + .withPureLogger(ToolkitLogger.console[IO](_)) + .withPureConfig(TestConfig.defaultTest) .withoutDependencies .beforeProvidingSeq( _ => logger.append(Event.Custom("beforeProviding_1")), @@ -326,8 +326,8 @@ class AppSuite extends munit.CatsEffectSuite { for { _ <- App[IO] .withInfo(TestAppInfo.value) - .withLogger(ToolkitLogger.console[IO](_)) - .withConfig(TestConfig.defaultTest) + .withPureLogger(ToolkitLogger.console[IO](_)) + .withPureConfig(TestConfig.defaultTest) .withoutDependencies .beforeProvidingSeq(_ => List( @@ -389,8 +389,8 @@ class AppSuite extends munit.CatsEffectSuite { state <- IO.ref[Boolean](false) _ <- App[IO] .withInfo(TestAppInfo.value) - .withLogger(ToolkitLogger.console[IO](_)) - .withConfig(TestConfig.defaultTest) + .withPureLogger(ToolkitLogger.console[IO](_)) + .withPureConfig(TestConfig.defaultTest) .withoutDependencies .provideOne(r => state.set( @@ -423,8 +423,8 @@ class AppSuite extends munit.CatsEffectSuite { state <- IO.ref[Boolean](false) app <- App[IO, AppError] .withInfo(TestAppInfo.value) - .withLogger(ToolkitLogger.console[IO](_)) - .withConfig(TestConfig.defaultTest) + .withPureLogger(ToolkitLogger.console[IO](_)) + .withPureConfig(TestConfig.defaultTest) .withoutDependencies .provide(_ => List( @@ -465,8 +465,8 @@ class AppSuite extends munit.CatsEffectSuite { state <- IO.ref[Boolean](false) app <- App[IO, AppError] .withInfo(TestAppInfo.value) - .withLogger(ToolkitLogger.console[IO](_)) - .withConfig(TestConfig.defaultTest) + .withPureLogger(ToolkitLogger.console[IO](_)) + .withPureConfig(TestConfig.defaultTest) .withoutDependencies .provide { _ => List( diff --git a/docs/compiled/README.md b/docs/compiled/README.md index db23f59..e14bbc1 100644 --- a/docs/compiled/README.md +++ b/docs/compiled/README.md @@ -127,8 +127,8 @@ object Main extends IOApp { sbtVersion = "1.8.0" ) ) - .withLogger(ToolkitLogger.console[IO](_)) - .withConfigLoader(_ => IO.pure(Config("localhost", 8080))) + .withPureLogger(ToolkitLogger.console[IO](_)) + .withConfigF(_ => IO.pure(Config("localhost", 8080))) .dependsOn(AppDependencyServices.resource(_)) .beforeProviding(_.logger.info("CUSTOM PRE-PROVIDING")) .provideOne(deps => diff --git a/docs/compiled/integrations.md b/docs/compiled/integrations.md index 6376cfa..b78f784 100644 --- a/docs/compiled/integrations.md +++ b/docs/compiled/integrations.md @@ -69,7 +69,7 @@ App[IO] sbtVersion = "1.8.0" ) ) - .withConfigLoader(pureconfigLoader[IO, TestConfig]) + .withConfigF(pureconfigLoader[IO, TestConfig]) .withoutDependencies .provideOne(_ => IO.unit) .run_ @@ -172,7 +172,7 @@ App[IO] sbtVersion = "1.8.0" ) ) - .withConfig( + .withPureConfig( TestConfig( dbUrl = "jdbc:postgresql://localhost:5432/toolkit", dbUser = Some("postgres"), diff --git a/docs/source/README.md b/docs/source/README.md index d587244..f8dcfe2 100644 --- a/docs/source/README.md +++ b/docs/source/README.md @@ -127,8 +127,8 @@ object Main extends IOApp { sbtVersion = "1.8.0" ) ) - .withLogger(ToolkitLogger.console[IO](_)) - .withConfigLoader(_ => IO.pure(Config("localhost", 8080))) + .withPureLogger(ToolkitLogger.console[IO](_)) + .withConfigF(_ => IO.pure(Config("localhost", 8080))) .dependsOn(AppDependencyServices.resource(_)) .beforeProviding(_.logger.info("CUSTOM PRE-PROVIDING")) .provideOne(deps => diff --git a/docs/source/integrations.md b/docs/source/integrations.md index f1dcfbf..bb0852e 100644 --- a/docs/source/integrations.md +++ b/docs/source/integrations.md @@ -69,7 +69,7 @@ App[IO] sbtVersion = "1.8.0" ) ) - .withConfigLoader(pureconfigLoader[IO, TestConfig]) + .withConfigF(pureconfigLoader[IO, TestConfig]) .withoutDependencies .provideOne(_ => IO.unit) .run_ @@ -172,7 +172,7 @@ App[IO] sbtVersion = "1.8.0" ) ) - .withConfig( + .withPureConfig( TestConfig( dbUrl = "jdbc:postgresql://localhost:5432/toolkit", dbUser = Some("postgres"), diff --git a/examples/buildinfo.properties b/examples/buildinfo.properties index 7e3e406..7b516d9 100644 --- a/examples/buildinfo.properties +++ b/examples/buildinfo.properties @@ -1,2 +1,2 @@ -#Sun Jul 09 14:35:34 CEST 2023 -buildnumber=210 +#Tue Jan 23 17:01:31 CET 2024 +buildnumber=211 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 4f7678e..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: AppResources[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/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/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-3/com/geirolz/example/app/AppDependencyServices.scala b/examples/src/main/scala/com/geirolz/example/app/AppDependencyServices.scala similarity index 92% rename from examples/src/main/scala-3/com/geirolz/example/app/AppDependencyServices.scala rename to examples/src/main/scala/com/geirolz/example/app/AppDependencyServices.scala index 56ccf26..44a7a5e 100644 --- a/examples/src/main/scala-3/com/geirolz/example/app/AppDependencyServices.scala +++ b/examples/src/main/scala/com/geirolz/example/app/AppDependencyServices.scala @@ -1,7 +1,7 @@ package com.geirolz.example.app import cats.effect.{IO, Resource} -import com.geirolz.app.toolkit.App +import com.geirolz.app.toolkit.{App, AppResources} import com.geirolz.app.toolkit.novalues.NoResources import com.geirolz.example.app.provided.KafkaConsumer import org.typelevel.log4cats.SelfAwareStructuredLogger 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 100% 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 diff --git a/examples/src/main/scala-3/com/geirolz/example/app/AppMain.scala b/examples/src/main/scala/com/geirolz/example/app/AppMain.scala similarity index 90% rename from examples/src/main/scala-3/com/geirolz/example/app/AppMain.scala rename to examples/src/main/scala/com/geirolz/example/app/AppMain.scala index 2ae716d..2eb3381 100644 --- a/examples/src/main/scala-3/com/geirolz/example/app/AppMain.scala +++ b/examples/src/main/scala/com/geirolz/example/app/AppMain.scala @@ -11,8 +11,8 @@ 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]) + .withPureLogger(Slf4jLogger.getLogger[IO]) + .withConfigF(pureconfigLoader[IO, AppConfig]) .dependsOn(AppDependencyServices.resource(_)) .beforeProviding(_.logger.info("CUSTOM PRE-RUN")) .provide(deps => diff --git a/examples/src/main/scala-2/com/geirolz/example/app/AppWithFailures.scala b/examples/src/main/scala/com/geirolz/example/app/AppWithFailures.scala similarity index 79% rename from examples/src/main/scala-2/com/geirolz/example/app/AppWithFailures.scala rename to examples/src/main/scala/com/geirolz/example/app/AppWithFailures.scala index 49f4d7c..27f4d3e 100644 --- a/examples/src/main/scala-2/com/geirolz/example/app/AppWithFailures.scala +++ b/examples/src/main/scala/com/geirolz/example/app/AppWithFailures.scala @@ -1,18 +1,19 @@ package com.geirolz.example.app import cats.effect.{ExitCode, IO, IOApp} -import com.geirolz.app.toolkit.App +import com.geirolz.app.toolkit.{App, AppMessages} import com.geirolz.app.toolkit.config.pureconfig.pureconfigLoader +import com.geirolz.app.toolkit.logger.given import com.geirolz.example.app.provided.AppHttpServer import org.typelevel.log4cats.slf4j.Slf4jLogger -object AppWithFailures extends IOApp { +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]) + .withPureLogger(Slf4jLogger.getLogger[IO]) + .withConfigF(pureconfigLoader[IO, AppConfig]) .dependsOn(AppDependencyServices.resource(_)) .beforeProviding(_.logger.info("CUSTOM PRE-RUN")) .provide(deps => @@ -30,4 +31,3 @@ object AppWithFailures extends IOApp { ) .onFinalize(_.logger.info("CUSTOM END")) .run(args) -} 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-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/package.scala b/integrations/fly4s/src/main/scala/com/geirolz/app/toolkit/fly4s/package.scala index bd6fceb..f6b9f6f 100644 --- 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 @@ -9,13 +9,13 @@ package object fly4s { import cats.syntax.all.* - def migrateDatabaseWithConfig[F[_]: Async, APP_INFO <: SimpleAppInfo[?], LOGGER_T[_[_]]: LoggerAdapter, CONFIG, DEPENDENCIES, RESOURCES]( + def migrateDatabaseWithConfig[F[_]: Async, 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 - ): AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] = + ): AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] = migrateDatabaseWith( url = d => url(d.config), user = d => user(d.config), @@ -24,13 +24,13 @@ package object fly4s { classLoader = classLoader ) - def migrateDatabaseWith[F[_]: Async, APP_INFO <: SimpleAppInfo[?], LOGGER_T[_[_]]: LoggerAdapter, CONFIG, DEPENDENCIES, RESOURCES]( - url: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => String, - user: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => Option[String] = (_: Any) => None, - password: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => Option[Array[Char]] = (_: Any) => None, - config: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => Fly4sConfig = (_: Any) => Fly4sConfig.default, + def migrateDatabaseWith[F[_]: Async, INFO <: SimpleAppInfo[?], LOGGER_T[_[_]]: LoggerAdapter, CONFIG, DEPENDENCIES, RESOURCES]( + url: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => String, + user: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => Option[String] = (_: Any) => None, + password: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => Option[Array[Char]] = (_: Any) => None, + config: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => Fly4sConfig = (_: Any) => Fly4sConfig.default, classLoader: ClassLoader = Thread.currentThread.getContextClassLoader - ): AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] = + ): AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] = migrateDatabase(dep => Fly4s .make[F]( @@ -42,9 +42,9 @@ package object fly4s { ) ) - def migrateDatabase[F[_]: Async, APP_INFO <: SimpleAppInfo[?], LOGGER_T[_[_]]: LoggerAdapter, CONFIG, DEPENDENCIES, RESOURCES]( - f: AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => Resource[F, Fly4s[F]] - ): AppDependencies[APP_INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] = + def migrateDatabase[F[_]: Async, INFO <: SimpleAppInfo[?], LOGGER_T[_[_]]: LoggerAdapter, CONFIG, DEPENDENCIES, RESOURCES]( + f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => Resource[F, Fly4s[F]] + ): AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] = dep => f(dep) .evalMap(fl4s => 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..e65242e 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 @@ -16,7 +16,7 @@ class Fly4sSupportSuite extends munit.CatsEffectSuite { sbtVersion = "1.8.0" ) ) - .withConfig( + .withPureConfig( TestConfig( dbUrl = "jdbc:postgresql://localhost:5432/toolkit", dbUser = Some("postgres"), 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..9908737 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,7 +19,7 @@ class Log4CatsLoggerAdapterSuite extends munit.CatsEffectSuite { sbtVersion = "1.8.0" ) ) - .withLogger(NoOpLogger[IO]) + .withPureLogger(NoOpLogger[IO]) .withoutDependencies .provideOne(_ => IO.unit) .run_ 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..8048b5d 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 @@ -18,7 +18,7 @@ class OdinLoggerAdapterSuite extends munit.CatsEffectSuite { sbtVersion = "1.8.0" ) ) - .withLogger(Logger.noop[IO]) + .withPureLogger(Logger.noop[IO]) .withoutDependencies .provideOne(_ => IO.unit) .run_ 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/test/scala/com/geirolz/app/toolkit/config/PureconfigSecretSupportSuite.scala b/integrations/pureconfig/src/test/scala/com/geirolz/app/toolkit/config/PureconfigSecretSupportSuite.scala index b46dceb..edf6140 100644 --- 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 @@ -21,7 +21,7 @@ class PureconfigSecretSupportSuite extends munit.CatsEffectSuite { sbtVersion = "1.8.0" ) ) - .withConfigLoader(pureconfigLoader[IO, TestConfig]) + .withConfigF(pureconfigLoader[IO, TestConfig]) .withoutDependencies .provideOne(_ => IO.unit) .run_ From 2ba95ef22139fb10fb148d4b59f526a3a3ebe6e7 Mon Sep 17 00:00:00 2001 From: geirolz Date: Wed, 24 Jan 2024 00:41:13 +0100 Subject: [PATCH 06/22] With Implicit context --- build.sbt | 1 + .../scala/com/geirolz/app/toolkit/=:!=.scala | 17 ++ .../scala/com/geirolz/app/toolkit/App.scala | 184 +++++++++--------- .../com/geirolz/app/toolkit/AppArgs.scala | 45 +++-- .../com/geirolz/app/toolkit/AppBuilder.scala | 115 +++++------ .../com/geirolz/app/toolkit/AppCompiler.scala | 14 +- .../{AppResources.scala => AppContext.scala} | 10 +- .../geirolz/app/toolkit/AppDependencies.scala | 4 +- .../app/toolkit/AppLogicInterpreter.scala | 44 +++++ .../app/toolkit/AppLogicTypeInterpreter.scala | 44 ----- .../com/geirolz/app/toolkit/AppMessages.scala | 2 +- .../geirolz/app/toolkit/FailureHandler.scala | 2 +- .../app/toolkit/novalues/NoFailure.scala | 7 + .../scala/com/geirolz/app/toolkit/types.scala | 4 +- .../AppResourcesAndDependenciesSuite.scala | 17 +- .../com/geirolz/app/toolkit/AppSuite.scala | 25 +-- .../app/toolkit/error/ErrorSyntaxSuite.scala | 3 +- .../toolkit/error/MultiExceptionSuite.scala | 3 +- .../app/toolkit/logger/AnsiValueSuite.scala | 5 +- .../app/toolkit/testing/TestAppInfo.scala | 6 +- .../app/toolkit/testing/TestConfig.scala | 7 +- docs/compiled/README.md | 2 +- docs/source/README.md | 2 +- examples/buildinfo.properties | 4 +- .../example/app/AppDependencyServices.scala | 7 +- .../com/geirolz/example/app/AppInfo.scala | 5 +- .../com/geirolz/example/app/AppMain.scala | 11 +- .../geirolz/example/app/AppWithFailures.scala | 8 +- .../app/toolkit/fly4s/Fly4sSupportSuite.scala | 2 +- .../logger/Log4CatsLoggerAdapterSuite.scala | 3 +- .../logger/OdinLoggerAdapterSuite.scala | 3 +- .../toolkit/config/pureconfig/package.scala | 19 -- .../app/toolkit/config/pureconfig/tasks.scala | 15 ++ .../config/PureconfigSecretSupportSuite.scala | 59 ------ .../app/toolkit/testing/EventLogger.scala | 28 ++- .../geirolz/app/toolkit/testing/events.scala | 32 ++- 36 files changed, 348 insertions(+), 411 deletions(-) create mode 100644 core/src/main/scala/com/geirolz/app/toolkit/=:!=.scala rename core/src/main/scala/com/geirolz/app/toolkit/{AppResources.scala => AppContext.scala} (81%) create mode 100644 core/src/main/scala/com/geirolz/app/toolkit/AppLogicInterpreter.scala delete mode 100644 core/src/main/scala/com/geirolz/app/toolkit/AppLogicTypeInterpreter.scala create mode 100644 core/src/main/scala/com/geirolz/app/toolkit/novalues/NoFailure.scala delete mode 100644 integrations/pureconfig/src/main/scala/com/geirolz/app/toolkit/config/pureconfig/package.scala create mode 100644 integrations/pureconfig/src/main/scala/com/geirolz/app/toolkit/config/pureconfig/tasks.scala delete mode 100644 integrations/pureconfig/src/test/scala/com/geirolz/app/toolkit/config/PureconfigSecretSupportSuite.scala diff --git a/build.sbt b/build.sbt index 9405d3f..5d7c038 100644 --- a/build.sbt +++ b/build.sbt @@ -202,6 +202,7 @@ lazy val baseSettings: Seq[Def.Setting[_]] = Seq( def scalacSettings(scalaVersion: String): Seq[String] = Seq( "-encoding", + "-deprecation", "utf-8", // Specify character encoding used by source files. "-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 diff --git a/core/src/main/scala/com/geirolz/app/toolkit/=:!=.scala b/core/src/main/scala/com/geirolz/app/toolkit/=:!=.scala new file mode 100644 index 0000000..c763bf1 --- /dev/null +++ b/core/src/main/scala/com/geirolz/app/toolkit/=:!=.scala @@ -0,0 +1,17 @@ +package com.geirolz.app.toolkit + +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/App.scala b/core/src/main/scala/com/geirolz/app/toolkit/App.scala index ae90640..6b7ad05 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/App.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/App.scala @@ -5,9 +5,9 @@ import cats.syntax.all.given import cats.{Endo, Foldable, Parallel, Show} import com.geirolz.app.toolkit.FailureHandler.OnFailureBehaviour import com.geirolz.app.toolkit.logger.LoggerAdapter - +import com.geirolz.app.toolkit.novalues.NoFailure +import com.geirolz.app.toolkit.novalues.NoFailure.NotNoFailure import scala.reflect.ClassTag -import scala.util.NotGiven class App[ F[+_]: Async: Parallel, @@ -23,104 +23,47 @@ class App[ val loggerBuilder: F[LOGGER_T[F]], val configLoader: Resource[F, CONFIG], val resourcesLoader: Resource[F, RESOURCES], - val beforeProvidingF: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit], - val onFinalizeF: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit], - val failureHandlerLoader: AppResources[INFO, LOGGER_T[F], CONFIG, RESOURCES] => FailureHandler[F, FAILURE], - val depsLoader: AppResources[INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, FAILURE \/ DEPENDENCIES], + val beforeProvidingTask: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit], + val onFinalizeTask: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit], + val failureHandlerLoader: AppContext[INFO, LOGGER_T[F], CONFIG, RESOURCES] ?=> FailureHandler[F, FAILURE], + val depsLoader: AppContext[INFO, LOGGER_T[F], CONFIG, RESOURCES] ?=> Resource[F, FAILURE \/ DEPENDENCIES], val servicesBuilder: AppDependencies[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 Self = App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] - private type SelfResources = AppResources[INFO, LOGGER_T[F], CONFIG, RESOURCES] - - class Resourced[T](val res: SelfResources, val value: T): - export res.* - def map[U](f: T => U): Resourced[U] = Resourced(res, f(value)) - def tupled: (SelfResources, T) = (res, value) - def useTupled[U](f: (SelfResources, T) => U): U = f.tupled(tupled) - def use[U](f: SelfResources => T => U): U = f(res)(value) - def tupledAll: (INFO, CONFIG, LOGGER_T[F], RESOURCES, T) = (info, config, logger, resources, value) - def useTupledAll[U](f: (INFO, CONFIG, LOGGER_T[F], RESOURCES, T) => U): U = f.tupled(tupledAll) - - def onFinalize( - f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] - ): Self = copyWith(onFinalizeF = f) - - def onFinalizeSeq( + type AppInfo = INFO + type Logger = LOGGER_T[F] + type Config = CONFIG + type Self = App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] + type Context = AppContext[INFO, LOGGER_T[F], CONFIG, RESOURCES] + + inline def onFinalizeSeq( f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit], fN: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit]* ): Self = onFinalizeSeq(deps => (f +: fN).map(_(deps))) - def onFinalizeSeq[G[_]: Foldable]( + inline def onFinalizeSeq[G[_]: Foldable]( f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => G[F[Unit]] ): Self = - copyWith(onFinalizeF = f(_).sequence_) - - // -------------------- AppFailureSyntax -------------------- - // failures - def mapFailure[FAILURE2]( - fhLoader: AppResources[INFO, LOGGER_T[F], CONFIG, RESOURCES] => FailureHandler[F, FAILURE2] - )( - f: FAILURE => FAILURE2 - )(using NotNothing[FAILURE], NotNothing[FAILURE2]): App[F, FAILURE2, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = - copyWith[F, FAILURE2, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES]( - failureHandlerLoader = fhLoader, - dependenciesLoader = depsLoader.andThen(_.map(_.leftMap(f))), - provideBuilder = servicesBuilder.andThen(_.map(_.leftMap(f).map(_.map(_.map(_.leftMap(f)))))) - ) - - def onFailure_( - f: Resourced[FAILURE] => F[Unit] - )(using NotNothing[FAILURE]): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = - _updateFailureHandlerLoader(appRes => _.onFailure(failure => f(Resourced(appRes, failure)) >> failureHandlerLoader(appRes).onFailureF(failure))) - - def onFailure( - f: Resourced[FAILURE] => F[OnFailureBehaviour] - )(using NotNothing[FAILURE]): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = - _updateFailureHandlerLoader(appRes => _.onFailure(f.compose(Resourced(appRes, _)))) - - def handleFailureWith( - f: Resourced[FAILURE] => F[FAILURE \/ Unit] - )(using NotNothing[FAILURE]): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = - _updateFailureHandlerLoader(appRes => _.handleFailureWith(f.compose(Resourced(appRes, _)))) + copyWith(onFinalizeTask = f(_).sequence_) // compile and run - def compile(appArgs: List[String])(using c: AppCompiler[F], i: AppLogicTypeInterpreter[F, FAILURE]): Resource[F, F[i.Result[Unit]]] = + 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)) - def run_(appArgs: List[String])(using AppCompiler[F], AppLogicTypeInterpreter[F, FAILURE]): F[Unit] = - run(appArgs).void - - def run(appArgs: List[String])(using c: AppCompiler[F], i: AppLogicTypeInterpreter[F, FAILURE]): F[ExitCode] = - runRaw(appArgs) + 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 ) - def runRaw(appArgs: List[String])(using c: AppCompiler[F], i: AppLogicTypeInterpreter[F, FAILURE]): F[i.Result[Unit]] = + inline def runRaw[R[_]](appArgs: List[String] = Nil)(using c: AppCompiler[F], i: AppLogicInterpreter[F, R, FAILURE]): F[R[Unit]] = compile(appArgs).useEval - private def _updateFailureHandlerLoader( - fh: AppResources[INFO, LOGGER_T[F], CONFIG, RESOURCES] => Endo[FailureHandler[F, FAILURE]] - ): App[ - F, - FAILURE, - INFO, - LOGGER_T, - CONFIG, - RESOURCES, - DEPENDENCIES - ] = - copyWith( - failureHandlerLoader = appRes => fh(appRes)(failureHandlerLoader(appRes)) - ) - - private def copyWith[ + private[toolkit] def copyWith[ G[+_]: Async: Parallel, FAILURE2, APP_INFO2 <: SimpleAppInfo[?], @@ -129,15 +72,15 @@ class App[ RES2, DEPS2 ]( - 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, - beforeProvidingF: AppDependencies[APP_INFO2, LOGGER_T2[G], CONFIG2, DEPS2, RES2] => G[Unit] = this.beforeProvidingF, - onFinalizeF: AppDependencies[APP_INFO2, LOGGER_T2[G], CONFIG2, DEPS2, RES2] => G[Unit] = this.onFinalizeF, - failureHandlerLoader: AppResources[APP_INFO2, LOGGER_T2[G], CONFIG2, RES2] => FailureHandler[G, FAILURE2] = this.failureHandlerLoader, - dependenciesLoader: AppResources[APP_INFO2, LOGGER_T2[G], CONFIG2, RES2] => Resource[G, FAILURE2 \/ DEPS2] = this.depsLoader, + 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: AppDependencies[APP_INFO2, LOGGER_T2[G], CONFIG2, DEPS2, RES2] => G[Unit] = this.beforeProvidingTask, + onFinalizeTask: AppDependencies[APP_INFO2, LOGGER_T2[G], CONFIG2, DEPS2, RES2] => G[Unit] = this.onFinalizeTask, + failureHandlerLoader: AppContext[APP_INFO2, LOGGER_T2[G], CONFIG2, RES2] ?=> FailureHandler[G, FAILURE2] = this.failureHandlerLoader, + dependenciesLoader: AppContext[APP_INFO2, LOGGER_T2[G], CONFIG2, RES2] ?=> Resource[G, FAILURE2 \/ DEPS2] = this.depsLoader, provideBuilder: AppDependencies[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]( @@ -146,17 +89,72 @@ class App[ loggerBuilder = loggerBuilder, configLoader = configLoader, resourcesLoader = resourcesLoader, - beforeProvidingF = beforeProvidingF, - onFinalizeF = onFinalizeF, + beforeProvidingTask = beforeProvidingTask, + onFinalizeTask = onFinalizeTask, failureHandlerLoader = failureHandlerLoader, depsLoader = dependenciesLoader, servicesBuilder = provideBuilder ) -object App: +object App extends AppFailureSyntax: + + inline def ctx[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, RESOURCES](using + c: AppContext[INFO, LOGGER, CONFIG, RESOURCES] + ): AppContext[INFO, LOGGER, CONFIG, RESOURCES] = c - def apply[F[+_]: Async: Parallel]: AppBuilder[F, Nothing] = + inline def apply[F[+_]: Async: Parallel]: AppBuilder[F, NoFailure] = AppBuilder[F] - def apply[F[+_]: Async: Parallel, FAILURE: ClassTag]: AppBuilder[F, FAILURE] = + inline def apply[F[+_]: Async: Parallel, FAILURE: ClassTag]: AppBuilder[F, FAILURE] = AppBuilder[F, FAILURE] + +sealed trait AppFailureSyntax: + + extension [ + F[+_]: Async: Parallel, + FAILURE: NotNoFailure, + INFO <: SimpleAppInfo[?], + LOGGER_T[_[_]]: LoggerAdapter, + CONFIG: Show, + RESOURCES, + DEPENDENCIES + ](app: App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES]) + + // failures + inline def mapFailure[FAILURE2]( + fhLoader: AppContext[INFO, LOGGER_T[F], CONFIG, RESOURCES] ?=> FailureHandler[F, FAILURE2] + )( + f: FAILURE => FAILURE2 + )(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.depsLoader.map(_.leftMap(f)), + provideBuilder = app.servicesBuilder.andThen(_.map(_.leftMap(f).map(_.map(_.map(_.leftMap(f)))))) + ) + + inline def onFailure_( + f: app.Context ?=> FAILURE => F[Unit] + ): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + _updateFailureHandlerLoader(_.onFailure(failure => f(failure) >> app.failureHandlerLoader.onFailureF(failure))) + + inline def onFailure( + f: app.Context ?=> FAILURE => F[OnFailureBehaviour] + ): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + _updateFailureHandlerLoader(_.onFailure(f)) + + inline def handleFailureWith( + f: app.Context ?=> FAILURE => F[FAILURE \/ Unit] + ): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = + _updateFailureHandlerLoader(_.handleFailureWith(f)) + + private def _updateFailureHandlerLoader( + fh: AppContext[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 c8742dd..89be574 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppArgs.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppArgs.scala @@ -7,40 +7,40 @@ import scala.util.Try 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,21 +58,23 @@ 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) - given Show[AppArgs] = Show.fromToString + inline def fromList(args: List[String]): AppArgs = AppArgs(args) + given Show[AppArgs] = Show.fromToString // --------------------------------- trait ArgDecoder[T]: @@ -80,13 +82,14 @@ trait ArgDecoder[T]: object ArgDecoder: - def apply[T: ArgDecoder]: ArgDecoder[T] = implicitly[ArgDecoder[T]] + 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) + inline def toException = new RuntimeException(toString) final override def toString: String = Show[ArgDecodingError].show(this) object ArgDecodingError: diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala index e0f6bb2..d722bab 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala @@ -1,11 +1,13 @@ package com.geirolz.app.toolkit -import cats.{Foldable, Parallel, Show} import cats.effect.{Async, Resource} +import cats.syntax.all.given +import cats.{Foldable, Parallel, Show} +import com.geirolz.app.toolkit.App.* import com.geirolz.app.toolkit.AppBuilder.SelectResAndDeps import com.geirolz.app.toolkit.logger.{LoggerAdapter, NoopLogger} -import com.geirolz.app.toolkit.novalues.{NoConfig, NoDependencies, NoResources} -import cats.syntax.all.given +import com.geirolz.app.toolkit.novalues.NoFailure.NotNoFailure +import com.geirolz.app.toolkit.novalues.{NoConfig, NoDependencies, NoFailure, NoResources} import scala.reflect.ClassTag @@ -24,11 +26,10 @@ final class AppBuilder[F[+_]: Async: Parallel, FAILURE: ClassTag]: object AppBuilder: - def apply[F[+_]: Async: Parallel]: AppBuilder[F, Nothing] = - given ClassTag[Nothing] = ClassTag.Nothing - new AppBuilder[F, Nothing] + inline def apply[F[+_]: Async: Parallel]: AppBuilder[F, NoFailure] = + new AppBuilder[F, NoFailure] - def apply[F[+_]: Async: Parallel, FAILURE: ClassTag: NotNothing]: AppBuilder[F, FAILURE] = + inline def apply[F[+_]: Async: Parallel, FAILURE: ClassTag: NotNoFailure]: AppBuilder[F, FAILURE] = new AppBuilder[F, FAILURE] final class SelectResAndDeps[ @@ -47,98 +48,97 @@ object AppBuilder: ): // ------- MESSAGES ------- - def withMessages(messages: AppMessages): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES] = + inline def withMessages(messages: AppMessages): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES] = copyWith(messages = messages) // ------- LOGGER ------- - def withNoopLogger: AppBuilder.SelectResAndDeps[F, FAILURE, INFO, NoopLogger, CONFIG, RESOURCES] = + inline def withNoopLogger: AppBuilder.SelectResAndDeps[F, FAILURE, INFO, NoopLogger, CONFIG, RESOURCES] = withPureLogger(logger = NoopLogger[F]) - def withPureLogger[LOGGER_T2[_[_]]: LoggerAdapter]( + inline def withPureLogger[LOGGER_T2[_[_]]: LoggerAdapter]( logger: LOGGER_T2[F] ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T2, CONFIG, RESOURCES] = withPureLogger[LOGGER_T2](f = (_: INFO) => logger) - def withPureLogger[LOGGER_T2[_[_]]: LoggerAdapter]( + inline def withPureLogger[LOGGER_T2[_[_]]: LoggerAdapter]( f: INFO => LOGGER_T2[F] ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T2, CONFIG, RESOURCES] = withLoggerF(f = appInfo => f(appInfo).pure[F]) // TODO: Add failure - def withLoggerF[LOGGER_T2[_[_]]: LoggerAdapter]( + inline def withLoggerF[LOGGER_T2[_[_]]: LoggerAdapter]( f: INFO => F[LOGGER_T2[F]] ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T2, CONFIG, RESOURCES] = copyWith(loggerBuilder = f(info)) // ------- CONFIG ------- - def withoutConfig: AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, NoConfig, RESOURCES] = + inline def withoutConfig: AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, NoConfig, RESOURCES] = withPureConfig[NoConfig](NoConfig.value) - def withPureConfig[CONFIG2: Show]( + inline def withPureConfig[CONFIG2: Show]( config: CONFIG2 ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG2, RESOURCES] = withConfigF(config.pure[F]) // TODO: Add failure - def withConfigF[CONFIG2: Show]( + 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 - def withConfigF[CONFIG2: Show]( + inline def withConfigF[CONFIG2: Show]( configLoader: F[CONFIG2] ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG2, RESOURCES] = withConfig(Resource.eval(configLoader)) // TODO: Add failure - def withConfig[CONFIG2: Show]( + inline def withConfig[CONFIG2: Show]( configLoader: Resource[F, CONFIG2] ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG2, RESOURCES] = withConfig(_ => configLoader) // TODO: Add failure - def withConfig[CONFIG2: Show]( + 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 ------- - def withoutResources: AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG, NoResources] = + inline def withoutResources: AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG, NoResources] = withPureResources[NoResources](NoResources.value) - def withPureResources[RESOURCES2]( + inline def withPureResources[RESOURCES2]( resources: RESOURCES2 ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES2] = withResourcesF(resources.pure[F]) // TODO: Add failure - def withResourcesF[RESOURCES2]( + inline def withResourcesF[RESOURCES2]( resourcesLoader: F[RESOURCES2] ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES2] = withResources(Resource.eval(resourcesLoader)) // TODO: Add failure - def withResources[RESOURCES2]( + inline def withResources[RESOURCES2]( resourcesLoader: Resource[F, RESOURCES2] ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES2] = copyWith(resourcesLoader = resourcesLoader) // ------- DEPENDENCIES ------- - def withoutDependencies: AppBuilder.SelectProvide[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, NoDependencies] = - dependsOn[NoDependencies, FAILURE](_ => Resource.pure(NoDependencies.value)) + inline def withoutDependencies: AppBuilder.SelectProvide[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, NoDependencies] = + dependsOn[NoDependencies, FAILURE](Resource.pure(NoDependencies.value)) - // TODO: Check ClassTag def dependsOn[DEPENDENCIES: ClassTag, FAILURE2 <: FAILURE: ClassTag]( - f: AppResources[INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, FAILURE2 | DEPENDENCIES] + f: AppContext[INFO, LOGGER_T[F], CONFIG, RESOURCES] ?=> Resource[F, FAILURE2 | DEPENDENCIES] ): AppBuilder.SelectProvide[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = - dependsOnE[DEPENDENCIES, FAILURE2](f.andThen(_.map { + dependsOnE[DEPENDENCIES, FAILURE2](f.map { case deps: DEPENDENCIES => Right(deps) case failure: FAILURE2 => Left(failure) - })) + }) def dependsOnE[DEPENDENCIES, FAILURE2 <: FAILURE]( - f: AppResources[INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, FAILURE2 \/ DEPENDENCIES] + f: AppContext[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, @@ -146,7 +146,7 @@ object AppBuilder: loggerBuilder = loggerBuilder, configLoader = configLoader, resourcesLoader = resourcesLoader, - dependenciesLoader = f, + dependenciesLoader = ctx => { given AppContext[INFO, LOGGER_T[F], CONFIG, RESOURCES] = ctx; f }, beforeProvidingF = _ => ().pure[F] ) @@ -175,31 +175,25 @@ object AppBuilder: RESOURCES, DEPENDENCIES ]( - private val info: INFO, - private val messages: AppMessages, - private val loggerBuilder: F[LOGGER_T[F]], - private val configLoader: Resource[F, CONFIG], - private val resourcesLoader: Resource[F, RESOURCES], - private val dependenciesLoader: AppResources[INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, FAILURE \/ DEPENDENCIES], - private val beforeProvidingF: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] + info: INFO, + messages: AppMessages, + loggerBuilder: F[LOGGER_T[F]], + configLoader: Resource[F, CONFIG], + resourcesLoader: Resource[F, RESOURCES], + dependenciesLoader: AppContext[INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, FAILURE \/ DEPENDENCIES], + beforeProvidingF: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] ): // ------- BEFORE PROVIDING ------- // TODO: Add failure - def beforeProviding( - f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] - ): AppBuilder.SelectProvide[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = - copy(beforeProvidingF = f) - - // TODO: Add failure - def beforeProvidingSeq( + inline def beforeProvidingSeq( f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit], fN: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit]* ): AppBuilder.SelectProvide[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = beforeProvidingSeq(deps => (f +: fN).map(_(deps))) // TODO: Add failure - def beforeProvidingSeq[G[_]: Foldable]( + inline def beforeProvidingSeq[G[_]: Foldable]( f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => G[F[Unit]] ): AppBuilder.SelectProvide[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = copy(beforeProvidingF = f(_).sequence_) @@ -213,12 +207,12 @@ object AppBuilder: case _: Unit => Right(()) })) - def provideOneE[FAILURE2 <: FAILURE]( + inline def provideOneE[FAILURE2 <: FAILURE]( f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[FAILURE2 \/ Unit] ): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = provideE[FAILURE2](f.andThen(List(_))) - def provideOneF[FAILURE2 <: FAILURE]( + inline def provideOneF[FAILURE2 <: FAILURE]( f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[FAILURE2 \/ F[Unit]] ): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = provideAttemptFE[FAILURE2](f.andThen(_.map(_.map(v => List(v.map(_.asRight[FAILURE2])))))) @@ -232,7 +226,7 @@ object AppBuilder: case _: Unit => Right(()) }))) - def provideE[FAILURE2 <: FAILURE]( + inline def provideE[FAILURE2 <: FAILURE]( f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => List[F[FAILURE2 \/ Unit]] )(using DummyImplicit): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = provideFE[FAILURE2](f.andThen(_.pure[F])) @@ -246,7 +240,7 @@ object AppBuilder: case _: Unit => Right(()) })))) - def provideFE[FAILURE2 <: FAILURE]( + inline def provideFE[FAILURE2 <: FAILURE]( f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[List[F[FAILURE2 \/ Unit]]] )(using DummyImplicit): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = provideAttemptFE(f.andThen(_.map(Right(_)))) @@ -259,16 +253,15 @@ object AppBuilder: new App( info = info, messages = messages, - failureHandlerLoader = res => - FailureHandler.logAndCancelAll[F, FAILURE]( - appMessages = res.messages, - logger = LoggerAdapter[LOGGER_T].toToolkit(res.logger) - ), - loggerBuilder = loggerBuilder, - resourcesLoader = resourcesLoader, - beforeProvidingF = beforeProvidingF, - onFinalizeF = _ => ().pure[F], - configLoader = configLoader, - depsLoader = dependenciesLoader, - servicesBuilder = f + failureHandlerLoader = FailureHandler.logAndCancelAll[F, FAILURE]( + appMessages = ctx.messages, + logger = LoggerAdapter[LOGGER_T].toToolkit(ctx.logger) + ), + loggerBuilder = loggerBuilder, + resourcesLoader = resourcesLoader, + beforeProvidingTask = beforeProvidingF, + onFinalizeTask = _ => ().pure[F], + configLoader = configLoader, + depsLoader = dependenciesLoader(ctx), + servicesBuilder = f ) diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppCompiler.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppCompiler.scala index 338d179..cb7f61b 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppCompiler.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppCompiler.scala @@ -2,9 +2,9 @@ package com.geirolz.app.toolkit import cats.data.{EitherT, NonEmptyList} import cats.effect.implicits.{genSpawnOps, monadCancelOps_} -import cats.effect.kernel.MonadCancelThrow import cats.effect.{Async, Fiber, Ref, Resource} import cats.{Parallel, Show} +import com.geirolz.app.toolkit.App.ctx import com.geirolz.app.toolkit.FailureHandler.OnFailureBehaviour import com.geirolz.app.toolkit.logger.LoggerAdapter @@ -55,7 +55,7 @@ object AppCompiler: otherResources <- EitherT.right[FAILURE](app.resourcesLoader) // group resources - appResources: AppResources[INFO, LOGGER_T[F], CONFIG, RESOURCES] = AppResources( + given AppContext[INFO, LOGGER_T[F], CONFIG, RESOURCES] = AppContext( info = app.info, messages = app.messages, args = AppArgs(appArgs), @@ -66,9 +66,9 @@ object AppCompiler: // ------------------- DEPENDENCIES ----------------- _ <- toolkitResLogger.debug(app.messages.buildingServicesEnv) - appDepServices <- EitherT(app.depsLoader(appResources)) + appDepServices <- EitherT(app.depsLoader) _ <- toolkitResLogger.info(app.messages.servicesEnvSuccessfullyBuilt) - appDependencies = AppDependencies(appResources, appDepServices) + appDependencies = AppDependencies(ctx, appDepServices) // --------------------- SERVICES ------------------- _ <- toolkitResLogger.debug(app.messages.buildingApp) @@ -79,7 +79,7 @@ object AppCompiler: 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(_) @@ -110,12 +110,12 @@ object AppCompiler: } yield maybeReducedFailures.toLeft(()) } yield { toolkitLogger.info(app.messages.startingApp) >> - app.beforeProvidingF(appDependencies) >> + app.beforeProvidingTask(appDependencies) >> appLogic .onCancel(toolkitLogger.info(app.messages.appWasStopped)) .onError(e => toolkitLogger.error(e)(app.messages.appAnErrorOccurred)) .guarantee( - app.onFinalizeF(appDependencies) >> toolkitLogger.info(app.messages.shuttingDownApp) + app.onFinalizeTask(appDependencies) >> toolkitLogger.info(app.messages.shuttingDownApp) ) } ).value diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppResources.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppContext.scala similarity index 81% rename from core/src/main/scala/com/geirolz/app/toolkit/AppResources.scala rename to core/src/main/scala/com/geirolz/app/toolkit/AppContext.scala index e30b658..32ede18 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppResources.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppContext.scala @@ -2,7 +2,7 @@ package com.geirolz.app.toolkit import cats.syntax.all.given -final case class AppResources[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, RESOURCES]( +final case class AppContext[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, RESOURCES]( info: INFO, messages: AppMessages, args: AppArgs, @@ -25,7 +25,7 @@ final case class AppResources[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, RESOURCE |)""".stripMargin } -object AppResources: +object AppContext: private[toolkit] def apply[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, RESOURCES]( info: INFO, @@ -34,8 +34,8 @@ object AppResources: logger: LOGGER, config: CONFIG, resources: RESOURCES - ): AppResources[INFO, LOGGER, CONFIG, RESOURCES] = - new AppResources[INFO, LOGGER, CONFIG, RESOURCES]( + ): AppContext[INFO, LOGGER, CONFIG, RESOURCES] = + new AppContext[INFO, LOGGER, CONFIG, RESOURCES]( info = info, messages = messages, args = args, @@ -45,7 +45,7 @@ object AppResources: ) def unapply[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, RESOURCES]( - res: AppResources[INFO, LOGGER, CONFIG, RESOURCES] + res: AppContext[INFO, LOGGER, CONFIG, RESOURCES] ): Option[(INFO, AppMessages, AppArgs, LOGGER, CONFIG, RESOURCES)] = ( res.info, diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppDependencies.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppDependencies.scala index 6c3c5be..a97e123 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppDependencies.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppDependencies.scala @@ -3,7 +3,7 @@ package com.geirolz.app.toolkit import cats.syntax.all.given final case class AppDependencies[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, DEPENDENCIES, RESOURCES]( - private val _resources: AppResources[INFO, LOGGER, CONFIG, RESOURCES], + private val _resources: AppContext[INFO, LOGGER, CONFIG, RESOURCES], private val _dependencies: DEPENDENCIES ): @@ -23,7 +23,7 @@ final case class AppDependencies[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, DEPEN object AppDependencies: private[toolkit] def apply[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, DEPENDENCIES, RESOURCES]( - resources: AppResources[INFO, LOGGER, CONFIG, RESOURCES], + resources: AppContext[INFO, LOGGER, CONFIG, RESOURCES], dependencies: DEPENDENCIES ): AppDependencies[INFO, LOGGER, CONFIG, DEPENDENCIES, RESOURCES] = new AppDependencies[INFO, LOGGER, CONFIG, DEPENDENCIES, RESOURCES]( 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/AppLogicTypeInterpreter.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppLogicTypeInterpreter.scala deleted file mode 100644 index f6f6547..0000000 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppLogicTypeInterpreter.scala +++ /dev/null @@ -1,44 +0,0 @@ -package com.geirolz.app.toolkit - -import cats.{Applicative, Functor, MonadThrow} -import cats.data.NonEmptyList -import cats.effect.Resource - -trait AppLogicTypeInterpreter[F[_], FAILURE]: - type Result[T] - def interpret[T](appLogic: Resource[F, FAILURE \/ F[NonEmptyList[FAILURE] \/ T]]): Resource[F, F[Result[T]]] - def isSuccess[T](value: Result[T]): Boolean - -object AppLogicTypeInterpreter: - - import cats.syntax.all.* - - def apply[F[_], FAILURE](using i: AppLogicTypeInterpreter[F, FAILURE]): AppLogicTypeInterpreter[F, FAILURE] = i - - given [F[_]: Applicative, FAILURE: NotNothing]: AppLogicTypeInterpreter[F, FAILURE] = - new AppLogicTypeInterpreter[F, FAILURE]: - override type Result[T] = NonEmptyList[FAILURE] \/ T - 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 - } - - given [F[_]: MonadThrow]: AppLogicTypeInterpreter[F, Nothing] = - new AppLogicTypeInterpreter[F, Nothing]: - override type Result[T] = T - override def isSuccess[T](value: T): Boolean = true - override def interpret[T](appLogic: Resource[F, Nothing \/ F[NonEmptyList[Nothing] \/ T]]): Resource[F, F[T]] = - appLogic.map { - case Left(_) => - MonadThrow[F].raiseError(new RuntimeException("Unreachable point.")) - case Right(value: F[NonEmptyList[Nothing] \/ T]) => - value.flatMap { - case Left(_) => - MonadThrow[F].raiseError(new RuntimeException("Unreachable point.")) - case Right(value) => - value.pure[F] - } - } 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 53d06be..15d98d0 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppMessages.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppMessages.scala @@ -15,7 +15,7 @@ case class AppMessages( ) object AppMessages: - def fromAppInfo[INFO <: SimpleAppInfo[?]](info: INFO)( + inline def fromAppInfo[INFO <: SimpleAppInfo[?]](info: INFO)( f: INFO => AppMessages ): AppMessages = f(info) diff --git a/core/src/main/scala/com/geirolz/app/toolkit/FailureHandler.scala b/core/src/main/scala/com/geirolz/app/toolkit/FailureHandler.scala index d9ff9e8..3381a39 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/FailureHandler.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/FailureHandler.scala @@ -33,7 +33,7 @@ case class FailureHandler[F[_], FAILURE]( } object FailureHandler extends FailureHandlerSyntax: - def summon[F[_], E](implicit ev: FailureHandler[F, E]): FailureHandler[F, E] = ev + inline def apply[F[_], E](using ev: FailureHandler[F, E]): FailureHandler[F, E] = ev def logAndCancelAll[F[_]: Monad, FAILURE](appMessages: AppMessages, logger: ToolkitLogger[F]): FailureHandler[F, FAILURE] = doNothing[F, FAILURE]().onFailure(failure => logger.error(s"${appMessages.appAFailureOccurred} $failure").as(OnFailureBehaviour.CancelAll)) 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..2bd8115 --- /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.=:!= + +sealed trait NoFailure +object NoFailure: + type NotNoFailure[T] = T =:!= NoFailure diff --git a/core/src/main/scala/com/geirolz/app/toolkit/types.scala b/core/src/main/scala/com/geirolz/app/toolkit/types.scala index d11e80f..57c54b5 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/types.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/types.scala @@ -1,8 +1,6 @@ package com.geirolz.app.toolkit import scala.annotation.targetName -import scala.util.NotGiven @targetName("Either") -type \/[+A, +B] = Either[A, B] -type NotNothing[T] = NotGiven[T =:= Nothing] \ No newline at end of file +type \/[+A, +B] = Either[A, B] diff --git a/core/src/test/scala/com/geirolz/app/toolkit/AppResourcesAndDependenciesSuite.scala b/core/src/test/scala/com/geirolz/app/toolkit/AppResourcesAndDependenciesSuite.scala index a39f3d7..d62df54 100644 --- a/core/src/test/scala/com/geirolz/app/toolkit/AppResourcesAndDependenciesSuite.scala +++ b/core/src/test/scala/com/geirolz/app/toolkit/AppResourcesAndDependenciesSuite.scala @@ -4,19 +4,19 @@ 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 { +class AppResourcesAndDependenciesSuite extends munit.FunSuite: // false positive not exhaustive pattern matching ? TODO: investigate test("AppResources unapply works as expected") { - App[IO] + val res = App[IO] .withInfo(TestAppInfo.value) .withPureLogger(ToolkitLogger.console[IO](_)) .withPureConfig(TestConfig.defaultTest) - .dependsOn { case _ | AppResources(_, _, _, _, _) => - Resource.eval(IO.unit) - } + .withoutResources + .withoutDependencies .provideOne(_ => IO.unit) - .run_ + .run() + .void } // false positive not exhaustive pattern matching ? TODO: investigate @@ -29,7 +29,6 @@ class AppResourcesAndDependenciesSuite extends munit.FunSuite { .provideOne { case _ | AppDependencies(_, _, _, _, _, _) => IO.unit } - .run_ + .run() + .void } - -} 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 deb558c..60d9c30 100644 --- a/core/src/test/scala/com/geirolz/app/toolkit/AppSuite.scala +++ b/core/src/test/scala/com/geirolz/app/toolkit/AppSuite.scala @@ -2,6 +2,7 @@ package com.geirolz.app.toolkit import cats.data.NonEmptyList import cats.effect.{IO, Ref, Resource} +import com.geirolz.app.toolkit.App.ctx import com.geirolz.app.toolkit.FailureHandler.OnFailureBehaviour import com.geirolz.app.toolkit.logger.ToolkitLogger import com.geirolz.app.toolkit.testing.{LabeledResource, *} @@ -24,7 +25,7 @@ class AppSuite extends munit.CatsEffectSuite { .withInfo(TestAppInfo.value) .withPureLogger(ToolkitLogger.console[IO](_)) .withPureConfig(TestConfig.defaultTest) - .dependsOn(_ => Resource.pure[IO, Ref[IO, Int]](counter).trace(LabeledResource.appDependencies)) + .dependsOn(Resource.pure[IO, Ref[IO, Int]](counter).trace(LabeledResource.appDependencies)) .provideOne(_.dependencies.set(1)) .compile() .runFullTracedApp @@ -67,7 +68,7 @@ class AppSuite extends munit.CatsEffectSuite { .withInfo(TestAppInfo.value) .withPureLogger(ToolkitLogger.console[IO](_)) .withPureConfig(TestConfig.defaultTest) - .dependsOn(_ => Resource.pure[IO, Unit](()).trace(LabeledResource.appDependencies)) + .dependsOn(Resource.pure[IO, Unit](()).trace(LabeledResource.appDependencies)) .provideOneF(_ => IO.raiseError(ex"BOOM!")) .compile() .traceAsAppLoader @@ -227,7 +228,7 @@ class AppSuite extends munit.CatsEffectSuite { .withInfo(TestAppInfo.value) .withPureLogger(ToolkitLogger.console[IO](_)) .withPureConfig(TestConfig.defaultTest) - .dependsOn(_ => Resource.pure[IO, Unit](()).trace(LabeledResource.appDependencies)) + .dependsOn(Resource.pure[IO, Unit](()).trace(LabeledResource.appDependencies)) .provideOne(_ => IO.raiseError(ex"BOOM!")) .compile() .runFullTracedApp @@ -322,7 +323,7 @@ class AppSuite extends munit.CatsEffectSuite { EventLogger .create[IO] .flatMap(logger => { - implicit val loggerImplicit: EventLogger[IO] = logger + given EventLogger[IO] = logger for { _ <- App[IO] .withInfo(TestAppInfo.value) @@ -418,7 +419,7 @@ 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] @@ -426,18 +427,14 @@ class AppSuite extends munit.CatsEffectSuite { .withPureLogger(ToolkitLogger.console[IO](_)) .withPureConfig(TestConfig.defaultTest) .withoutDependencies - .provide(_ => + .provideE(_ => 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) @@ -468,16 +465,14 @@ class AppSuite extends munit.CatsEffectSuite { .withPureLogger(ToolkitLogger.console[IO](_)) .withPureConfig(TestConfig.defaultTest) .withoutDependencies - .provide { _ => + .provideE { _ => 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..4639ba3 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,7 @@ package com.geirolz.app.toolkit.error -class ErrorSyntaxSuite extends munit.FunSuite { +class ErrorSyntaxSuite extends munit.FunSuite: test("MultiError") { ex"BOOM!".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 e14bbc1..67ed15c 100644 --- a/docs/compiled/README.md +++ b/docs/compiled/README.md @@ -139,7 +139,7 @@ object Main extends IOApp { .compile .drain ) - .onFinalize(_.logger.info("CUSTOM END")) + .onFinalizeSeq(_.logger.info("CUSTOM END")) .run(args) } ``` diff --git a/docs/source/README.md b/docs/source/README.md index f8dcfe2..2274ec1 100644 --- a/docs/source/README.md +++ b/docs/source/README.md @@ -139,7 +139,7 @@ object Main extends IOApp { .compile .drain ) - .onFinalize(_.logger.info("CUSTOM END")) + .onFinalizeSeq(_.logger.info("CUSTOM END")) .run(args) } ``` diff --git a/examples/buildinfo.properties b/examples/buildinfo.properties index 7b516d9..1530a6a 100644 --- a/examples/buildinfo.properties +++ b/examples/buildinfo.properties @@ -1,2 +1,2 @@ -#Tue Jan 23 17:01:31 CET 2024 -buildnumber=211 +#Wed Jan 24 00:39:35 CET 2024 +buildnumber=230 diff --git a/examples/src/main/scala/com/geirolz/example/app/AppDependencyServices.scala b/examples/src/main/scala/com/geirolz/example/app/AppDependencyServices.scala index 44a7a5e..b183c16 100644 --- a/examples/src/main/scala/com/geirolz/example/app/AppDependencyServices.scala +++ b/examples/src/main/scala/com/geirolz/example/app/AppDependencyServices.scala @@ -1,8 +1,9 @@ package com.geirolz.example.app import cats.effect.{IO, Resource} -import com.geirolz.app.toolkit.{App, AppResources} +import com.geirolz.app.toolkit.App.ctx import com.geirolz.app.toolkit.novalues.NoResources +import com.geirolz.app.toolkit.{App, AppContext} import com.geirolz.example.app.provided.KafkaConsumer import org.typelevel.log4cats.SelfAwareStructuredLogger @@ -11,9 +12,9 @@ case class AppDependencyServices( ) object AppDependencyServices: - def resource(res: AppResources[AppInfo, SelfAwareStructuredLogger[IO], AppConfig, NoResources]): Resource[IO, AppDependencyServices] = + def resource(using AppContext[AppInfo, SelfAwareStructuredLogger[IO], AppConfig, NoResources]): Resource[IO, AppDependencyServices] = Resource.pure( AppDependencyServices( - KafkaConsumer.fake(res.config.kafkaBroker.host) + KafkaConsumer.fake(ctx.config.kafkaBroker.host) ) ) diff --git a/examples/src/main/scala/com/geirolz/example/app/AppInfo.scala b/examples/src/main/scala/com/geirolz/example/app/AppInfo.scala index b760961..d1a14a6 100644 --- a/examples/src/main/scala/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 index 2eb3381..4ef1132 100644 --- a/examples/src/main/scala/com/geirolz/example/app/AppMain.scala +++ b/examples/src/main/scala/com/geirolz/example/app/AppMain.scala @@ -2,9 +2,12 @@ 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.App.ctx import com.geirolz.app.toolkit.config.pureconfig.* +import com.geirolz.app.toolkit.logger.log4CatsLoggerAdapter +import com.geirolz.app.toolkit.novalues.NoResources import com.geirolz.example.app.provided.AppHttpServer +import org.typelevel.log4cats.SelfAwareStructuredLogger import org.typelevel.log4cats.slf4j.Slf4jLogger object AppMain extends IOApp: @@ -13,8 +16,8 @@ object AppMain extends IOApp: .withInfo(AppInfo.fromBuildInfo) .withPureLogger(Slf4jLogger.getLogger[IO]) .withConfigF(pureconfigLoader[IO, AppConfig]) - .dependsOn(AppDependencyServices.resource(_)) - .beforeProviding(_.logger.info("CUSTOM PRE-RUN")) + .dependsOn(AppDependencyServices.resource) + .beforeProvidingSeq(_.logger.info("CUSTOM PRE-RUN")) .provide(deps => List( // HTTP server @@ -28,5 +31,5 @@ object AppMain extends IOApp: .drain ) ) - .onFinalize(_.logger.info("CUSTOM END")) + .onFinalizeSeq(_.logger.info("CUSTOM END")) .run(args) diff --git a/examples/src/main/scala/com/geirolz/example/app/AppWithFailures.scala b/examples/src/main/scala/com/geirolz/example/app/AppWithFailures.scala index 27f4d3e..84a9e86 100644 --- a/examples/src/main/scala/com/geirolz/example/app/AppWithFailures.scala +++ b/examples/src/main/scala/com/geirolz/example/app/AppWithFailures.scala @@ -14,8 +14,8 @@ object AppWithFailures extends IOApp: .withInfo(AppInfo.fromBuildInfo) .withPureLogger(Slf4jLogger.getLogger[IO]) .withConfigF(pureconfigLoader[IO, AppConfig]) - .dependsOn(AppDependencyServices.resource(_)) - .beforeProviding(_.logger.info("CUSTOM PRE-RUN")) + .dependsOn(AppDependencyServices.resource) + .beforeProvidingSeq(_.logger.info("CUSTOM PRE-RUN")) .provide(deps => List( // HTTP server @@ -29,5 +29,5 @@ object AppWithFailures extends IOApp: .drain ) ) - .onFinalize(_.logger.info("CUSTOM END")) - .run(args) + .onFinalizeSeq(_.logger.info("CUSTOM END")) + .run() 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 e65242e..8cf2315 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 @@ -24,7 +24,7 @@ class Fly4sSupportSuite extends munit.CatsEffectSuite { ) ) .withoutDependencies - .beforeProviding( + .beforeProvidingSeq( migrateDatabaseWithConfig( url = _.dbUrl, user = _.dbUser, 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 9908737..7a806f7 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 @@ -22,7 +22,8 @@ class Log4CatsLoggerAdapterSuite extends munit.CatsEffectSuite { .withPureLogger(NoOpLogger[IO]) .withoutDependencies .provideOne(_ => IO.unit) - .run_ + .run() + .void ) } 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 8048b5d..556ff20 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 @@ -21,7 +21,8 @@ class OdinLoggerAdapterSuite extends munit.CatsEffectSuite { .withPureLogger(Logger.noop[IO]) .withoutDependencies .provideOne(_ => IO.unit) - .run_ + .run() + .void ) } 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 edf6140..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" - ) - ) - .withConfigF(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/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..a57e0b1 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,14 @@ 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]) { + extension[F[_]: MonadCancelThrow: EventLogger, T](resource: Resource[F, T]) - def trace(labeledResource: LabeledResource): Resource[F, T] = { + def trace(labeledResource: LabeledResource): Resource[F, T] = val logger = EventLogger[F] resource .preAllocate(logger.append(labeledResource.starting)) @@ -47,6 +46,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..4fa3ec1 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) @@ -42,4 +41,3 @@ object LabeledResource { val appRuntime: LabeledResource = LabeledResource.uniqueResource("app-runtime") val appLoader: LabeledResource = LabeledResource.uniqueResource("app-loader") val appDependencies: LabeledResource = LabeledResource.uniqueResource("app-dependencies") -} From be74e737a3bbda066c0e4d6a909fd5c555c9ac22 Mon Sep 17 00:00:00 2001 From: geirolz Date: Thu, 25 Jan 2024 01:07:21 +0100 Subject: [PATCH 07/22] Fix build.sbt --- build.sbt | 4 ++-- .../main/scala/com/geirolz/app/toolkit/fly4s/package.scala | 2 +- .../scala/com/geirolz/app/toolkit/testing/EventLogger.scala | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/build.sbt b/build.sbt index 5d7c038..5beafa8 100644 --- a/build.sbt +++ b/build.sbt @@ -44,7 +44,7 @@ lazy val docs: Project = .settings( baseSettings, noPublishSettings, - libraryDependencies ++=ProjectDependencies.Docs.dedicated, + libraryDependencies ++= ProjectDependencies.Docs.dedicated, // config scalacOptions --= Seq("-Werror", "-Xfatal-warnings"), mdocIn := file("docs/source"), @@ -202,8 +202,8 @@ lazy val baseSettings: Seq[Def.Setting[_]] = Seq( def scalacSettings(scalaVersion: String): Seq[String] = Seq( "-encoding", - "-deprecation", "utf-8", // Specify character encoding used by source files. + "-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:higherKinds", // Allow higher-kinded types 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 index f6b9f6f..1651db0 100644 --- 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 @@ -29,7 +29,7 @@ package object fly4s { user: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => Option[String] = (_: Any) => None, password: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => Option[Array[Char]] = (_: Any) => None, config: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => Fly4sConfig = (_: Any) => Fly4sConfig.default, - classLoader: ClassLoader = Thread.currentThread.getContextClassLoader + classLoader: ClassLoader = Thread.currentThread.getContextClassLoader ): AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] = migrateDatabase(dep => Fly4s 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 a57e0b1..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 @@ -36,8 +36,7 @@ object EventLogger: def traceAsAppRuntime: F[Unit] = Resource.eval(app).trace(LabeledResource.appRuntime).use_ - extension[F[_]: MonadCancelThrow: EventLogger, T](resource: Resource[F, T]) - + extension [F[_]: MonadCancelThrow: EventLogger, T](resource: Resource[F, T]) def trace(labeledResource: LabeledResource): Resource[F, T] = val logger = EventLogger[F] resource From fca2002f4907a7b960b95f6c816354decbd5001b Mon Sep 17 00:00:00 2001 From: geirolz Date: Thu, 25 Jan 2024 12:44:07 +0100 Subject: [PATCH 08/22] Add transparent traits --- core/src/main/scala/com/geirolz/app/toolkit/App.scala | 2 +- .../scala/com/geirolz/app/toolkit/FailureHandler.scala | 2 +- .../com/geirolz/app/toolkit/console/AnsiValue.scala | 10 ++++------ 3 files changed, 6 insertions(+), 8 deletions(-) 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 6b7ad05..55bb9d8 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/App.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/App.scala @@ -108,7 +108,7 @@ object App extends AppFailureSyntax: inline def apply[F[+_]: Async: Parallel, FAILURE: ClassTag]: AppBuilder[F, FAILURE] = AppBuilder[F, FAILURE] -sealed trait AppFailureSyntax: +sealed transparent trait AppFailureSyntax: extension [ F[+_]: Async: Parallel, diff --git a/core/src/main/scala/com/geirolz/app/toolkit/FailureHandler.scala b/core/src/main/scala/com/geirolz/app/toolkit/FailureHandler.scala index 3381a39..e8da4e3 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/FailureHandler.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/FailureHandler.scala @@ -52,7 +52,7 @@ object FailureHandler extends FailureHandlerSyntax: case object CancelAll extends OnFailureBehaviour case object DoNothing extends OnFailureBehaviour -sealed trait FailureHandlerSyntax { +sealed transparent trait FailureHandlerSyntax { import cats.syntax.all.* 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 c479e99..1c2e227 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 @@ -272,16 +272,15 @@ object AnsiValue extends AnsiValueInstances with AnsiValueSyntax: final val INVISIBLE: AnsiValue.S = S(scala.Console.INVISIBLE) end AnsiValue -private[toolkit] sealed trait AnsiValueInstances: +private[toolkit] sealed transparent trait AnsiValueInstances: - given 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) - } given Show[AnsiValue] = Show.fromToString -private[toolkit] sealed trait AnsiValueSyntax { +private[toolkit] sealed transparent trait AnsiValueSyntax: extension (t: AnsiText) def print[F[_]: Console]: F[Unit] = Console[F].print(t) @@ -313,5 +312,4 @@ private[toolkit] sealed trait AnsiValueSyntax { def ansiUnderlined: AnsiText = ansiStyle(_.UNDERLINED) def ansiBlink: AnsiText = ansiStyle(_.BLINK) def ansiReversed: AnsiText = ansiStyle(_.REVERSED) - def ansiInvisible: AnsiText = ansiStyle(_.INVISIBLE) -} + def ansiInvisible: AnsiText = ansiStyle(_.INVISIBLE) \ No newline at end of file From 9f1cdbbb982ba7a465c3db667e908f7225bdf331 Mon Sep 17 00:00:00 2001 From: geirolz Date: Thu, 25 Jan 2024 13:08:01 +0100 Subject: [PATCH 09/22] Rename ex to error --- .../geirolz/app/toolkit/FailureHandler.scala | 38 ++++++++----------- .../app/toolkit/console/AnsiValue.scala | 4 +- .../app/toolkit/error/ErrorLifter.scala | 8 ++-- .../geirolz/app/toolkit/error/implicits.scala | 13 +++---- .../app/toolkit/logger/LoggerAdapter.scala | 5 ++- .../app/toolkit/novalues/NoFailure.scala | 2 +- .../app/toolkit/{ => utils}/=:!=.scala | 2 +- .../com/geirolz/app/toolkit/AppSuite.scala | 8 ++-- .../app/toolkit/error/ErrorSyntaxSuite.scala | 8 +++- .../logger/Log4CatsLoggerAdapterSuite.scala | 2 +- .../logger/OdinLoggerAdapterSuite.scala | 2 +- 11 files changed, 43 insertions(+), 49 deletions(-) rename core/src/main/scala/com/geirolz/app/toolkit/{ => utils}/=:!=.scala (92%) diff --git a/core/src/main/scala/com/geirolz/app/toolkit/FailureHandler.scala b/core/src/main/scala/com/geirolz/app/toolkit/FailureHandler.scala index e8da4e3..cdbda7f 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/FailureHandler.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/FailureHandler.scala @@ -9,12 +9,13 @@ import cats.syntax.all.* case class FailureHandler[F[_], FAILURE]( onFailureF: FAILURE => F[OnFailureBehaviour], handleFailureWithF: FAILURE => F[FAILURE \/ Unit] -) { $this => +): + $this => - def onFailure(f: FAILURE => F[OnFailureBehaviour]): FailureHandler[F, FAILURE] = + inline def onFailure(f: FAILURE => F[OnFailureBehaviour]): FailureHandler[F, FAILURE] = copy(onFailureF = f) - def handleFailureWith(f: FAILURE => F[FAILURE \/ Unit]): FailureHandler[F, FAILURE] = + inline def handleFailureWith(f: FAILURE => F[FAILURE \/ Unit]): FailureHandler[F, FAILURE] = copy(handleFailureWithF = f) def mapK[G[_]](f: F ~> G): FailureHandler[G, FAILURE] = @@ -26,11 +27,9 @@ case class FailureHandler[F[_], FAILURE]( def widen[EE <: FAILURE]: FailureHandler[F, EE] = this.asInstanceOf[FailureHandler[F, EE]] - def widenNel[EE](implicit - env: FAILURE =:= NonEmptyList[EE] - ): FailureHandler[F, NonEmptyList[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 @@ -52,18 +51,16 @@ object FailureHandler extends FailureHandlerSyntax: case object CancelAll extends OnFailureBehaviour case object DoNothing extends OnFailureBehaviour -sealed transparent trait FailureHandlerSyntax { +sealed transparent 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]] = + 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($this.onFailureF(_)) + .traverse(fh.onFailureF(_)) .map( _.collectFirst { case OnFailureBehaviour.CancelAll => OnFailureBehaviour.CancelAll @@ -71,22 +68,17 @@ sealed transparent trait FailureHandlerSyntax { ), handleFailureWithF = (failures: NonEmptyList[FAILURE]) => failures.toList - .traverse($this.handleFailureWithF(_)) + .traverse(fh.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] = + extension [F[+_], FAILURE](fh: FailureHandler[F, NonEmptyList[FAILURE]]) + def single(using 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)) + 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/console/AnsiValue.scala b/core/src/main/scala/com/geirolz/app/toolkit/console/AnsiValue.scala index 1c2e227..2b4e0f3 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 @@ -29,7 +29,7 @@ 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 = @@ -312,4 +312,4 @@ private[toolkit] sealed transparent trait AnsiValueSyntax: def ansiUnderlined: AnsiText = ansiStyle(_.UNDERLINED) def ansiBlink: AnsiText = ansiStyle(_.BLINK) def ansiReversed: AnsiText = ansiStyle(_.REVERSED) - def ansiInvisible: AnsiText = ansiStyle(_.INVISIBLE) \ No newline at end of file + 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 3375d28..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 @@ -13,12 +13,12 @@ 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)))) - } 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 index 19194f7..4caa0fa 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/error/implicits.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/error/implicits.scala @@ -3,15 +3,12 @@ package com.geirolz.app.toolkit.error import cats.kernel.Semigroup extension (ctx: StringContext) - def ex(args: Any*): RuntimeException = - new RuntimeException(ctx.s(args*)).dropFirstStackTraceElement + inline def error(args: Any*): RuntimeException = + new RuntimeException(ctx.s(args*)) -extension [T <: Throwable](ex: T) - def dropFirstStackTraceElement: T = - val stackTrace = ex.getStackTrace - if (stackTrace != null && stackTrace.length > 1) - ex.setStackTrace(stackTrace.tail) - ex +extension (str: String) + inline def asError: RuntimeException = + new RuntimeException(str) given Semigroup[Throwable] = (x: Throwable, y: Throwable) => (x, y) match 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 e34d99c..e063e8e 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 @@ -4,9 +4,10 @@ trait LoggerAdapter[LOGGER[_[_]]]: def toToolkit[F[_]](appLogger: LOGGER[F]): ToolkitLogger[F] object LoggerAdapter: - def apply[LOGGER[_[_]]: LoggerAdapter]: LoggerAdapter[LOGGER] = implicitly[LoggerAdapter[LOGGER]] + inline def apply[LOGGER[_[_]]: LoggerAdapter]: LoggerAdapter[LOGGER] = + summon[LoggerAdapter[LOGGER]] - implicit def id[L[K[_]] <: ToolkitLogger[K]]: LoggerAdapter[L] = + given [L[K[_]] <: ToolkitLogger[K]]: LoggerAdapter[L] = new LoggerAdapter[L]: override def toToolkit[F[_]](u: L[F]): ToolkitLogger[F] = new ToolkitLogger[F]: 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 index 2bd8115..077e594 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/novalues/NoFailure.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/novalues/NoFailure.scala @@ -1,6 +1,6 @@ package com.geirolz.app.toolkit.novalues -import com.geirolz.app.toolkit.=:!= +import com.geirolz.app.toolkit.utils.=:!= sealed trait NoFailure object NoFailure: diff --git a/core/src/main/scala/com/geirolz/app/toolkit/=:!=.scala b/core/src/main/scala/com/geirolz/app/toolkit/utils/=:!=.scala similarity index 92% rename from core/src/main/scala/com/geirolz/app/toolkit/=:!=.scala rename to core/src/main/scala/com/geirolz/app/toolkit/utils/=:!=.scala index c763bf1..be3414b 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/=:!=.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/utils/=:!=.scala @@ -1,4 +1,4 @@ -package com.geirolz.app.toolkit +package com.geirolz.app.toolkit.utils import scala.annotation.{implicitAmbiguous, implicitNotFound} import scala.language.postfixOps 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 60d9c30..a7f8a07 100644 --- a/core/src/test/scala/com/geirolz/app/toolkit/AppSuite.scala +++ b/core/src/test/scala/com/geirolz/app/toolkit/AppSuite.scala @@ -69,7 +69,7 @@ class AppSuite extends munit.CatsEffectSuite { .withPureLogger(ToolkitLogger.console[IO](_)) .withPureConfig(TestConfig.defaultTest) .dependsOn(Resource.pure[IO, Unit](()).trace(LabeledResource.appDependencies)) - .provideOneF(_ => IO.raiseError(ex"BOOM!")) + .provideOneF(_ => IO.raiseError(error"BOOM!")) .compile() .traceAsAppLoader .attempt @@ -229,7 +229,7 @@ class AppSuite extends munit.CatsEffectSuite { .withPureLogger(ToolkitLogger.console[IO](_)) .withPureConfig(TestConfig.defaultTest) .dependsOn(Resource.pure[IO, Unit](()).trace(LabeledResource.appDependencies)) - .provideOne(_ => IO.raiseError(ex"BOOM!")) + .provideOne(_ => IO.raiseError(error"BOOM!")) .compile() .runFullTracedApp .attempt @@ -440,9 +440,9 @@ class AppSuite extends munit.CatsEffectSuite { } 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) 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 4639ba3..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 @@ -2,6 +2,10 @@ package com.geirolz.app.toolkit.error 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/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 7a806f7..a126944 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 @@ -32,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/test/scala/com/geirolz/app/toolkit/logger/OdinLoggerAdapterSuite.scala b/integrations/odin/src/test/scala/com/geirolz/app/toolkit/logger/OdinLoggerAdapterSuite.scala index 556ff20..8e16b44 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 @@ -31,7 +31,7 @@ class OdinLoggerAdapterSuite extends munit.CatsEffectSuite { val tkLogger = adapterLogger.toToolkit(Logger.noop[IO]) assertIO_( - tkLogger.info("msg") >> tkLogger.error(ex"BOOM!")("msg") + tkLogger.info("msg") >> tkLogger.error(error"BOOM!")("msg") ) } } From ca0d325e5f8f737a9d98a43274393cbbc32c935c Mon Sep 17 00:00:00 2001 From: geirolz Date: Thu, 25 Jan 2024 13:23:12 +0100 Subject: [PATCH 10/22] Add scoverage --- .github/workflows/cicd.yml | 9 +++++---- project/plugins.sbt | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 01c6010..034279d 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 }} @@ -30,7 +29,7 @@ jobs: include: - scala: 3.3.1 name: Scala3_3 - test-tasks: scalafmtCheck test gen-doc + test-tasks: scalafmtCheck gen-doc coverage test coverageReport steps: - uses: actions/checkout@v3 @@ -62,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/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") From bbb3030949fa7f062bdf84dbd45eb52d822543e2 Mon Sep 17 00:00:00 2001 From: geirolz Date: Thu, 25 Jan 2024 14:45:57 +0100 Subject: [PATCH 11/22] Fix import --- examples/src/main/scala/com/geirolz/example/app/AppMain.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/src/main/scala/com/geirolz/example/app/AppMain.scala b/examples/src/main/scala/com/geirolz/example/app/AppMain.scala index 4ef1132..62bd76e 100644 --- a/examples/src/main/scala/com/geirolz/example/app/AppMain.scala +++ b/examples/src/main/scala/com/geirolz/example/app/AppMain.scala @@ -2,7 +2,6 @@ package com.geirolz.example.app import cats.effect.{ExitCode, IO, IOApp} import com.geirolz.app.toolkit.App -import com.geirolz.app.toolkit.App.ctx import com.geirolz.app.toolkit.config.pureconfig.* import com.geirolz.app.toolkit.logger.log4CatsLoggerAdapter import com.geirolz.app.toolkit.novalues.NoResources From 3843a9f971a70ba9a2df610afc8243ce2d7affaa Mon Sep 17 00:00:00 2001 From: geirolz Date: Tue, 30 Jan 2024 11:01:25 +0100 Subject: [PATCH 12/22] Make beforeProviding and onFinalize in append mode --- core/src/main/scala/com/geirolz/app/toolkit/App.scala | 6 ++++-- .../main/scala/com/geirolz/app/toolkit/AppBuilder.scala | 3 ++- .../main/scala/com/geirolz/app/toolkit/AppCompiler.scala | 2 +- .../app/toolkit/{ => failure}/FailureHandler.scala | 9 +++++---- .../test/scala/com/geirolz/app/toolkit/AppSuite.scala | 2 +- 5 files changed, 13 insertions(+), 9 deletions(-) rename core/src/main/scala/com/geirolz/app/toolkit/{ => failure}/FailureHandler.scala (95%) 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 55bb9d8..88a8c77 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/App.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/App.scala @@ -3,10 +3,12 @@ package com.geirolz.app.toolkit import cats.effect.* import cats.syntax.all.given import cats.{Endo, Foldable, Parallel, Show} -import com.geirolz.app.toolkit.FailureHandler.OnFailureBehaviour +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 import com.geirolz.app.toolkit.novalues.NoFailure.NotNoFailure + import scala.reflect.ClassTag class App[ @@ -44,7 +46,7 @@ class App[ inline def onFinalizeSeq[G[_]: Foldable]( f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => G[F[Unit]] ): Self = - copyWith(onFinalizeTask = f(_).sequence_) + copyWith(onFinalizeTask = d => this.onFinalizeTask(d) >> f(d).sequence_) // compile and run inline def compile[R[_]]( diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala index d722bab..c098a07 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala @@ -5,6 +5,7 @@ import cats.syntax.all.given import cats.{Foldable, Parallel, Show} 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.{LoggerAdapter, NoopLogger} import com.geirolz.app.toolkit.novalues.NoFailure.NotNoFailure import com.geirolz.app.toolkit.novalues.{NoConfig, NoDependencies, NoFailure, NoResources} @@ -196,7 +197,7 @@ object AppBuilder: inline def beforeProvidingSeq[G[_]: Foldable]( f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => G[F[Unit]] ): AppBuilder.SelectProvide[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = - copy(beforeProvidingF = f(_).sequence_) + copy(beforeProvidingF = d => this.beforeProvidingF(d) >> f(d).sequence_) // ------- PROVIDE ------- def provideOne[FAILURE2 <: FAILURE: ClassTag]( diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppCompiler.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppCompiler.scala index cb7f61b..b0c48db 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppCompiler.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppCompiler.scala @@ -5,7 +5,7 @@ import cats.effect.implicits.{genSpawnOps, monadCancelOps_} import cats.effect.{Async, Fiber, Ref, Resource} import cats.{Parallel, Show} import com.geirolz.app.toolkit.App.ctx -import com.geirolz.app.toolkit.FailureHandler.OnFailureBehaviour +import com.geirolz.app.toolkit.failure.FailureHandler.OnFailureBehaviour import com.geirolz.app.toolkit.logger.LoggerAdapter trait AppCompiler[F[+_]]: diff --git a/core/src/main/scala/com/geirolz/app/toolkit/FailureHandler.scala b/core/src/main/scala/com/geirolz/app/toolkit/failure/FailureHandler.scala similarity index 95% rename from core/src/main/scala/com/geirolz/app/toolkit/FailureHandler.scala rename to core/src/main/scala/com/geirolz/app/toolkit/failure/FailureHandler.scala index cdbda7f..b05a1fa 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/FailureHandler.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/failure/FailureHandler.scala @@ -1,10 +1,11 @@ -package com.geirolz.app.toolkit +package com.geirolz.app.toolkit.failure -import cats.{~>, Applicative, Functor, Monad, Show} import cats.data.NonEmptyList -import com.geirolz.app.toolkit.FailureHandler.OnFailureBehaviour -import com.geirolz.app.toolkit.logger.ToolkitLogger 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.ToolkitLogger case class FailureHandler[F[_], FAILURE]( onFailureF: FAILURE => F[OnFailureBehaviour], 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 a7f8a07..287d191 100644 --- a/core/src/test/scala/com/geirolz/app/toolkit/AppSuite.scala +++ b/core/src/test/scala/com/geirolz/app/toolkit/AppSuite.scala @@ -3,7 +3,7 @@ package com.geirolz.app.toolkit import cats.data.NonEmptyList import cats.effect.{IO, Ref, Resource} import com.geirolz.app.toolkit.App.ctx -import com.geirolz.app.toolkit.FailureHandler.OnFailureBehaviour +import com.geirolz.app.toolkit.failure.FailureHandler.OnFailureBehaviour import com.geirolz.app.toolkit.logger.ToolkitLogger import com.geirolz.app.toolkit.testing.{LabeledResource, *} From f57cbc69430257d10d3cf3e1bda6ebb1781cf278 Mon Sep 17 00:00:00 2001 From: geirolz Date: Tue, 30 Jan 2024 11:38:08 +0100 Subject: [PATCH 13/22] Fix Context naming --- README.md | 2 +- .../com/geirolz/app/toolkit/AppContext.scala | 2 +- .../geirolz/app/toolkit/AppDependencies.scala | 6 +++--- ...a => AppContextAndDependenciesSuite.scala} | 4 ++-- docs/compiled/README.md | 6 +++--- docs/source/README.md | 20 +++++++------------ docs/source/integrations.md | 19 +++++++++--------- 7 files changed, 27 insertions(+), 32 deletions(-) rename core/src/test/scala/com/geirolz/app/toolkit/{AppResourcesAndDependenciesSuite.scala => AppContextAndDependenciesSuite.scala} (89%) diff --git a/README.md b/README.md index db23f59..7570658 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ object Config { case class AppDependencyServices(kafkaConsumer: KafkaConsumer[IO]) object AppDependencyServices { - def resource(res: AppResources[SimpleAppInfo[String], ToolkitLogger[IO], Config, NoResources]): Resource[IO, AppDependencyServices] = + def resource(res: AppContext[SimpleAppInfo[String], ToolkitLogger[IO], Config, NoResources]): Resource[IO, AppDependencyServices] = Resource.pure(AppDependencyServices(KafkaConsumer.fake)) } diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppContext.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppContext.scala index 32ede18..d9e3242 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppContext.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppContext.scala @@ -16,7 +16,7 @@ final case class AppContext[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, RESOURCES] type Resources = RESOURCES override def toString: String = - s"""AppResources( + s"""AppContext( | info = $info, | args = $args, | logger = $logger, diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppDependencies.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppDependencies.scala index a97e123..729af34 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppDependencies.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppDependencies.scala @@ -3,11 +3,11 @@ package com.geirolz.app.toolkit import cats.syntax.all.given final case class AppDependencies[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, DEPENDENCIES, RESOURCES]( - private val _resources: AppContext[INFO, LOGGER, CONFIG, RESOURCES], + private val _context: AppContext[INFO, LOGGER, CONFIG, RESOURCES], private val _dependencies: DEPENDENCIES ): - export _resources.{args, config, info, logger, resources} + export _context.* val dependencies: DEPENDENCIES = _dependencies override def toString: String = @@ -27,7 +27,7 @@ object AppDependencies: dependencies: DEPENDENCIES ): AppDependencies[INFO, LOGGER, CONFIG, DEPENDENCIES, RESOURCES] = new AppDependencies[INFO, LOGGER, CONFIG, DEPENDENCIES, RESOURCES]( - _resources = resources, + _context = resources, _dependencies = dependencies ) diff --git a/core/src/test/scala/com/geirolz/app/toolkit/AppResourcesAndDependenciesSuite.scala b/core/src/test/scala/com/geirolz/app/toolkit/AppContextAndDependenciesSuite.scala similarity index 89% rename from core/src/test/scala/com/geirolz/app/toolkit/AppResourcesAndDependenciesSuite.scala rename to core/src/test/scala/com/geirolz/app/toolkit/AppContextAndDependenciesSuite.scala index d62df54..1a5b2cb 100644 --- a/core/src/test/scala/com/geirolz/app/toolkit/AppResourcesAndDependenciesSuite.scala +++ b/core/src/test/scala/com/geirolz/app/toolkit/AppContextAndDependenciesSuite.scala @@ -4,10 +4,10 @@ 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: +class AppContextAndDependenciesSuite extends munit.FunSuite: // false positive not exhaustive pattern matching ? TODO: investigate - test("AppResources unapply works as expected") { + test("AppContext unapply works as expected") { val res = App[IO] .withInfo(TestAppInfo.value) .withPureLogger(ToolkitLogger.console[IO](_)) diff --git a/docs/compiled/README.md b/docs/compiled/README.md index 67ed15c..8698a3c 100644 --- a/docs/compiled/README.md +++ b/docs/compiled/README.md @@ -84,10 +84,10 @@ object Config { // Define service dependencies case class AppDependencyServices(kafkaConsumer: KafkaConsumer[IO]) -object AppDependencyServices { - def resource(res: AppResources[SimpleAppInfo[String], ToolkitLogger[IO], Config, NoResources]): Resource[IO, AppDependencyServices] = +object AppDependencyServices: + def resource(res: AppContext[SimpleAppInfo[String], ToolkitLogger[IO], Config, NoResources]): Resource[IO, AppDependencyServices] = Resource.pure(AppDependencyServices(KafkaConsumer.fake)) -} + // A stubbed kafka consumer trait KafkaConsumer[F[_]] { diff --git a/docs/source/README.md b/docs/source/README.md index 2274ec1..3ed1929 100644 --- a/docs/source/README.md +++ b/docs/source/README.md @@ -76,25 +76,21 @@ 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: AppResources[SimpleAppInfo[String], ToolkitLogger[IO], Config, NoResources]): Resource[IO, AppDependencyServices] = +object AppDependencyServices: + def resource(res: AppContext[SimpleAppInfo[String], ToolkitLogger[IO], Config, NoResources]): 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 @@ -116,7 +111,7 @@ import cats.effect.{ExitCode, IO, IOApp} import com.geirolz.app.toolkit.{App, SimpleAppInfo} import com.geirolz.app.toolkit.logger.ToolkitLogger -object Main extends IOApp { +object Main extends IOApp: override def run(args: List[String]): IO[ExitCode] = App[IO] .withInfo( @@ -141,7 +136,6 @@ object Main extends IOApp { ) .onFinalizeSeq(_.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 bb0852e..2e629c2 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( @@ -72,7 +72,8 @@ App[IO] .withConfigF(pureconfigLoader[IO, TestConfig]) .withoutDependencies .provideOne(_ => IO.unit) - .run_ + .run() + .void ``` ## [Log4cats](https://github.com/typelevel/log4cats) @@ -159,9 +160,8 @@ import com.geirolz.app.toolkit.{App, SimpleAppInfo} 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( @@ -188,5 +188,6 @@ App[IO] ) ) .provideOne(_ => IO.unit) - .run_ + .run() + .void ``` \ No newline at end of file From d4bbaa6f755b36a360b55773892c382ec054bdd6 Mon Sep 17 00:00:00 2001 From: geirolz Date: Wed, 31 Jan 2024 10:08:28 +0100 Subject: [PATCH 14/22] Improve logger --- .../scala/com/geirolz/app/toolkit/App.scala | 6 +- .../com/geirolz/app/toolkit/AppBuilder.scala | 39 ++++--- .../app/toolkit/console/AnsiValue.scala | 23 ++++ .../app/toolkit/failure/FailureHandler.scala | 6 +- .../app/toolkit/logger/ConsoleLogger.scala | 67 +++++++++++ .../geirolz/app/toolkit/logger/Logger.scala | 68 +++++++++++ .../app/toolkit/logger/LoggerAdapter.scala | 32 +++--- .../app/toolkit/logger/NoopLogger.scala | 24 ++-- .../app/toolkit/logger/ToolkitLogger.scala | 106 ------------------ .../AppContextAndDependenciesSuite.scala | 6 +- .../com/geirolz/app/toolkit/AppSuite.scala | 26 ++--- docs/compiled/README.md | 14 ++- docs/source/README.md | 54 ++++----- examples/buildinfo.properties | 4 +- .../geirolz/app/toolkit/logger/package.scala | 26 +++-- .../geirolz/app/toolkit/logger/package.scala | 26 +++-- 16 files changed, 299 insertions(+), 228 deletions(-) create mode 100644 core/src/main/scala/com/geirolz/app/toolkit/logger/ConsoleLogger.scala create mode 100644 core/src/main/scala/com/geirolz/app/toolkit/logger/Logger.scala delete mode 100644 core/src/main/scala/com/geirolz/app/toolkit/logger/ToolkitLogger.scala 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 88a8c77..272e222 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/App.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/App.scala @@ -117,9 +117,9 @@ sealed transparent trait AppFailureSyntax: FAILURE: NotNoFailure, INFO <: SimpleAppInfo[?], LOGGER_T[_[_]]: LoggerAdapter, - CONFIG: Show, - RESOURCES, - DEPENDENCIES + CONFIG: ClassTag: Show, + RESOURCES: ClassTag, + DEPENDENCIES: ClassTag ](app: App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES]) // failures diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala index c098a07..c718aeb 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala @@ -6,7 +6,8 @@ import cats.{Foldable, Parallel, Show} 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.{LoggerAdapter, NoopLogger} +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} @@ -56,6 +57,9 @@ object AppBuilder: inline def withNoopLogger: AppBuilder.SelectResAndDeps[F, FAILURE, INFO, NoopLogger, CONFIG, RESOURCES] = withPureLogger(logger = NoopLogger[F]) + inline def withConsoleLogger(minLevel: Level = Level.Info): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, ConsoleLogger, CONFIG, RESOURCES] = + withPureLogger(logger = ConsoleLogger[F](info, minLevel)) + inline def withPureLogger[LOGGER_T2[_[_]]: LoggerAdapter]( logger: LOGGER_T2[F] ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T2, CONFIG, RESOURCES] = @@ -130,7 +134,7 @@ object AppBuilder: inline def withoutDependencies: AppBuilder.SelectProvide[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, NoDependencies] = dependsOn[NoDependencies, FAILURE](Resource.pure(NoDependencies.value)) - def dependsOn[DEPENDENCIES: ClassTag, FAILURE2 <: FAILURE: ClassTag]( + inline def dependsOn[DEPENDENCIES: ClassTag, FAILURE2 <: FAILURE: ClassTag]( f: AppContext[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 { @@ -142,18 +146,23 @@ object AppBuilder: f: AppContext[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 = ctx => { given AppContext[INFO, LOGGER_T[F], CONFIG, RESOURCES] = ctx; f }, - beforeProvidingF = _ => ().pure[F] + info = info, + messages = messages, + loggerBuilder = loggerBuilder, + configLoader = configLoader, + resourcesLoader = resourcesLoader, + dependenciesLoader = ctx => { given AppContext[INFO, LOGGER_T[F], CONFIG, RESOURCES] = ctx; f }, + beforeProvidingTask = _ => ().pure[F] ) - private def copyWith[G[+_]: Async: Parallel, FAILURE2: ClassTag, INFO2 <: SimpleAppInfo[?], LOGGER_T2[ - _[_] - ]: LoggerAdapter, CONFIG2: Show, RESOURCES2]( + 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, @@ -182,7 +191,7 @@ object AppBuilder: configLoader: Resource[F, CONFIG], resourcesLoader: Resource[F, RESOURCES], dependenciesLoader: AppContext[INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, FAILURE \/ DEPENDENCIES], - beforeProvidingF: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] + beforeProvidingTask: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] ): // ------- BEFORE PROVIDING ------- @@ -197,7 +206,7 @@ object AppBuilder: inline def beforeProvidingSeq[G[_]: Foldable]( f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => G[F[Unit]] ): AppBuilder.SelectProvide[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = - copy(beforeProvidingF = d => this.beforeProvidingF(d) >> f(d).sequence_) + copy(beforeProvidingTask = d => this.beforeProvidingTask(d) >> f(d).sequence_) // ------- PROVIDE ------- def provideOne[FAILURE2 <: FAILURE: ClassTag]( @@ -260,7 +269,7 @@ object AppBuilder: ), loggerBuilder = loggerBuilder, resourcesLoader = resourcesLoader, - beforeProvidingTask = beforeProvidingF, + beforeProvidingTask = beforeProvidingTask, onFinalizeTask = _ => ().pure[F], configLoader = configLoader, depsLoader = dependenciesLoader(ctx), 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 2b4e0f3..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,6 +24,17 @@ import com.geirolz.app.toolkit.console.AnsiValue.AnsiText * .withBackground(AnsiValue.B.BLACK) * .withStyle(AnsiValue.S.BLINK) * }}} + * + *
ForegroundBackground
BLACK BLACK_B
RED RED_B
GREEN GREEN_B
YELLOW YELLOW_B
BLUE BLUE_B
MAGENTAMAGENTA_B
CYAN CYAN_B
WHITE WHITE_B
*/ sealed trait AnsiValue: @@ -136,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 @@ -192,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 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 index b05a1fa..59a8092 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/failure/FailureHandler.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/failure/FailureHandler.scala @@ -5,7 +5,7 @@ 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.ToolkitLogger +import com.geirolz.app.toolkit.logger.Logger case class FailureHandler[F[_], FAILURE]( onFailureF: FAILURE => F[OnFailureBehaviour], @@ -35,8 +35,8 @@ 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: ToolkitLogger[F]): FailureHandler[F, FAILURE] = - doNothing[F, FAILURE]().onFailure(failure => logger.error(s"${appMessages.appAFailureOccurred} $failure").as(OnFailureBehaviour.CancelAll)) + 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]) 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..5848675 --- /dev/null +++ b/core/src/main/scala/com/geirolz/app/toolkit/logger/Logger.scala @@ -0,0 +1,68 @@ +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.* + + 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 e063e8e..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,23 +1,25 @@ package com.geirolz.app.toolkit.logger trait LoggerAdapter[LOGGER[_[_]]]: - def toToolkit[F[_]](appLogger: LOGGER[F]): ToolkitLogger[F] + def toToolkit[F[_]](appLogger: LOGGER[F]): Logger[F] object LoggerAdapter: - inline def apply[LOGGER[_[_]]: LoggerAdapter]: LoggerAdapter[LOGGER] = + inline def apply[LOGGER[_[_]]: LoggerAdapter]: LoggerAdapter[LOGGER] = summon[LoggerAdapter[LOGGER]] - given [L[K[_]] <: ToolkitLogger[K]]: LoggerAdapter[L] = + given [L[K[_]] <: Logger[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) + 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 8f7ea05..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,17 +2,19 @@ package com.geirolz.app.toolkit.logger import cats.Applicative -sealed trait NoopLogger[F[_]] extends ToolkitLogger[F] +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 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 + 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 399a31b..0000000 --- a/core/src/main/scala/com/geirolz/app/toolkit/logger/ToolkitLogger.scala +++ /dev/null @@ -1,106 +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 - - given Show[Level] = Show.fromToString - given 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/test/scala/com/geirolz/app/toolkit/AppContextAndDependenciesSuite.scala b/core/src/test/scala/com/geirolz/app/toolkit/AppContextAndDependenciesSuite.scala index 1a5b2cb..13accbd 100644 --- a/core/src/test/scala/com/geirolz/app/toolkit/AppContextAndDependenciesSuite.scala +++ b/core/src/test/scala/com/geirolz/app/toolkit/AppContextAndDependenciesSuite.scala @@ -1,7 +1,7 @@ package com.geirolz.app.toolkit import cats.effect.{IO, Resource} -import com.geirolz.app.toolkit.logger.ToolkitLogger +import com.geirolz.app.toolkit.logger.Logger import com.geirolz.app.toolkit.testing.{TestAppInfo, TestConfig} class AppContextAndDependenciesSuite extends munit.FunSuite: @@ -10,7 +10,7 @@ class AppContextAndDependenciesSuite extends munit.FunSuite: test("AppContext unapply works as expected") { val res = App[IO] .withInfo(TestAppInfo.value) - .withPureLogger(ToolkitLogger.console[IO](_)) + .withConsoleLogger() .withPureConfig(TestConfig.defaultTest) .withoutResources .withoutDependencies @@ -23,7 +23,7 @@ class AppContextAndDependenciesSuite extends munit.FunSuite: test("AppDependencies unapply works as expected") { App[IO] .withInfo(TestAppInfo.value) - .withPureLogger(ToolkitLogger.console[IO](_)) + .withConsoleLogger() .withPureConfig(TestConfig.defaultTest) .withoutDependencies .provideOne { case _ | AppDependencies(_, _, _, _, _, _) => 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 287d191..ade7779 100644 --- a/core/src/test/scala/com/geirolz/app/toolkit/AppSuite.scala +++ b/core/src/test/scala/com/geirolz/app/toolkit/AppSuite.scala @@ -4,8 +4,8 @@ import cats.data.NonEmptyList import cats.effect.{IO, Ref, Resource} import com.geirolz.app.toolkit.App.ctx import com.geirolz.app.toolkit.failure.FailureHandler.OnFailureBehaviour -import com.geirolz.app.toolkit.logger.ToolkitLogger -import com.geirolz.app.toolkit.testing.{LabeledResource, *} +import com.geirolz.app.toolkit.logger.Logger +import com.geirolz.app.toolkit.testing.* import scala.concurrent.duration.DurationInt @@ -23,7 +23,7 @@ class AppSuite extends munit.CatsEffectSuite { counter: Ref[IO, Int] <- IO.ref(0) _ <- App[IO] .withInfo(TestAppInfo.value) - .withPureLogger(ToolkitLogger.console[IO](_)) + .withPureLogger(Logger.console[IO](_)) .withPureConfig(TestConfig.defaultTest) .dependsOn(Resource.pure[IO, Ref[IO, Int]](counter).trace(LabeledResource.appDependencies)) .provideOne(_.dependencies.set(1)) @@ -66,7 +66,7 @@ class AppSuite extends munit.CatsEffectSuite { for { _ <- App[IO] .withInfo(TestAppInfo.value) - .withPureLogger(ToolkitLogger.console[IO](_)) + .withPureLogger(Logger.console[IO](_)) .withPureConfig(TestConfig.defaultTest) .dependsOn(Resource.pure[IO, Unit](()).trace(LabeledResource.appDependencies)) .provideOneF(_ => IO.raiseError(error"BOOM!")) @@ -102,7 +102,7 @@ class AppSuite extends munit.CatsEffectSuite { for { _ <- App[IO] .withInfo(TestAppInfo.value) - .withPureLogger(ToolkitLogger.console[IO](_)) + .withPureLogger(Logger.console[IO](_)) .withPureConfig(TestConfig.defaultTest) .withoutDependencies .provide(_ => @@ -144,7 +144,7 @@ class AppSuite extends munit.CatsEffectSuite { for { _ <- App[IO] .withInfo(TestAppInfo.value) - .withPureLogger(ToolkitLogger.console[IO](_)) + .withPureLogger(Logger.console[IO](_)) .withPureConfig(TestConfig.defaultTest) .withoutDependencies .provideOne(_ => IO.sleep(1.second)) @@ -180,7 +180,7 @@ class AppSuite extends munit.CatsEffectSuite { for { _ <- App[IO] .withInfo(TestAppInfo.value) - .withPureLogger(ToolkitLogger.console[IO](_)) + .withPureLogger(Logger.console[IO](_)) .withPureConfig(TestConfig.defaultTest) .withoutDependencies .provideF(_ => @@ -226,7 +226,7 @@ class AppSuite extends munit.CatsEffectSuite { _ <- App[IO] .withInfo(TestAppInfo.value) - .withPureLogger(ToolkitLogger.console[IO](_)) + .withPureLogger(Logger.console[IO](_)) .withPureConfig(TestConfig.defaultTest) .dependsOn(Resource.pure[IO, Unit](()).trace(LabeledResource.appDependencies)) .provideOne(_ => IO.raiseError(error"BOOM!")) @@ -266,7 +266,7 @@ class AppSuite extends munit.CatsEffectSuite { for { _ <- App[IO] .withInfo(TestAppInfo.value) - .withPureLogger(ToolkitLogger.console[IO](_)) + .withPureLogger(Logger.console[IO](_)) .withPureConfig(TestConfig.defaultTest) .withoutDependencies .beforeProvidingSeq( @@ -327,7 +327,7 @@ class AppSuite extends munit.CatsEffectSuite { for { _ <- App[IO] .withInfo(TestAppInfo.value) - .withPureLogger(ToolkitLogger.console[IO](_)) + .withPureLogger(Logger.console[IO](_)) .withPureConfig(TestConfig.defaultTest) .withoutDependencies .beforeProvidingSeq(_ => @@ -390,7 +390,7 @@ class AppSuite extends munit.CatsEffectSuite { state <- IO.ref[Boolean](false) _ <- App[IO] .withInfo(TestAppInfo.value) - .withPureLogger(ToolkitLogger.console[IO](_)) + .withPureLogger(Logger.console[IO](_)) .withPureConfig(TestConfig.defaultTest) .withoutDependencies .provideOne(r => @@ -424,7 +424,7 @@ class AppSuite extends munit.CatsEffectSuite { state <- IO.ref[Boolean](false) app <- App[IO, AppError] .withInfo(TestAppInfo.value) - .withPureLogger(ToolkitLogger.console[IO](_)) + .withPureLogger(Logger.console[IO](_)) .withPureConfig(TestConfig.defaultTest) .withoutDependencies .provideE(_ => @@ -462,7 +462,7 @@ class AppSuite extends munit.CatsEffectSuite { state <- IO.ref[Boolean](false) app <- App[IO, AppError] .withInfo(TestAppInfo.value) - .withPureLogger(ToolkitLogger.console[IO](_)) + .withPureLogger(Logger.console[IO](_)) .withPureConfig(TestConfig.defaultTest) .withoutDependencies .provideE { _ => diff --git a/docs/compiled/README.md b/docs/compiled/README.md index 8698a3c..3bdd826 100644 --- a/docs/compiled/README.md +++ b/docs/compiled/README.md @@ -71,7 +71,7 @@ libraryDependencies += "com.github.geirolz" %% "toolkit" % "0.0.11" 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.logger.Logger import com.geirolz.app.toolkit.novalues.NoResources // Define config @@ -84,9 +84,11 @@ object Config { // Define service dependencies case class AppDependencyServices(kafkaConsumer: KafkaConsumer[IO]) -object AppDependencyServices: - def resource(res: AppContext[SimpleAppInfo[String], ToolkitLogger[IO], Config, NoResources]): Resource[IO, AppDependencyServices] = - Resource.pure(AppDependencyServices(KafkaConsumer.fake)) +object AppDependencyServices + +: +def resource(res: AppContext[SimpleAppInfo[String], Logger[IO], Config, NoResources]): Resource[IO, AppDependencyServices] = + Resource.pure(AppDependencyServices(KafkaConsumer.fake)) // A stubbed kafka consumer @@ -114,7 +116,7 @@ object KafkaConsumer { ```scala import cats.effect.{ExitCode, IO, IOApp} import com.geirolz.app.toolkit.{App, SimpleAppInfo} -import com.geirolz.app.toolkit.logger.ToolkitLogger +import com.geirolz.app.toolkit.logger.Logger object Main extends IOApp { override def run(args: List[String]): IO[ExitCode] = @@ -127,7 +129,7 @@ object Main extends IOApp { sbtVersion = "1.8.0" ) ) - .withPureLogger(ToolkitLogger.console[IO](_)) + .withPureLogger(Logger.console[IO](_)) .withConfigF(_ => IO.pure(Config("localhost", 8080))) .dependsOn(AppDependencyServices.resource(_)) .beforeProviding(_.logger.info("CUSTOM PRE-PROVIDING")) diff --git a/docs/source/README.md b/docs/source/README.md index 3ed1929..0fd4b2a 100644 --- a/docs/source/README.md +++ b/docs/source/README.md @@ -71,7 +71,7 @@ libraryDependencies += "com.github.geirolz" %% "toolkit" % "@VERSION@" 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.logger.Logger import com.geirolz.app.toolkit.novalues.NoResources // Define config @@ -83,8 +83,8 @@ object Config: case class AppDependencyServices(kafkaConsumer: KafkaConsumer[IO]) object AppDependencyServices: - def resource(res: AppContext[SimpleAppInfo[String], ToolkitLogger[IO], Config, NoResources]): Resource[IO, AppDependencyServices] = - Resource.pure(AppDependencyServices(KafkaConsumer.fake)) + def resource(res: AppContext[SimpleAppInfo[String], Logger[IO], Config, NoResources]): Resource[IO, AppDependencyServices] = + Resource.pure(AppDependencyServices(KafkaConsumer.fake)) // A stubbed kafka consumer trait KafkaConsumer[F[_]]: @@ -109,33 +109,33 @@ 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 +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" + 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" + ) ) - ) - .withPureLogger(ToolkitLogger.console[IO](_)) - .withConfigF(_ => 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 - ) - .onFinalizeSeq(_.logger.info("CUSTOM END")) - .run(args) + .withPureLogger(Logger.console[IO](_)) + .withConfigF(_ => 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 + ) + .onFinalizeSeq(_.logger.info("CUSTOM END")) + .run(args) ``` Check a full example [here](https://github.com/geirolz/toolkit/tree/main/examples) diff --git a/examples/buildinfo.properties b/examples/buildinfo.properties index 1530a6a..e4fbb35 100644 --- a/examples/buildinfo.properties +++ b/examples/buildinfo.properties @@ -1,2 +1,2 @@ -#Wed Jan 24 00:39:35 CET 2024 -buildnumber=230 +#Tue Jan 30 18:16:36 CET 2024 +buildnumber=231 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 index 90ac091..2ef52b3 100644 --- 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 @@ -6,18 +6,20 @@ 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) + 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/odin/src/main/scala/com/geirolz/app/toolkit/logger/package.scala b/integrations/odin/src/main/scala/com/geirolz/app/toolkit/logger/package.scala index 2c790b7..f42d7c2 100644 --- 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 @@ -6,18 +6,20 @@ 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) + 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) } } } From d32c437d36e5a601e8a26b4767fe4b3b60aa6d85 Mon Sep 17 00:00:00 2001 From: geirolz Date: Wed, 31 Jan 2024 11:05:17 +0100 Subject: [PATCH 15/22] Improve logger --- build.sbt | 1 + .../com/geirolz/app/toolkit/AppBuilder.scala | 2 +- .../com/geirolz/app/toolkit/AppSuite.scala | 25 ++++++++++--------- .../com/geirolz/example/app/AppMain.scala | 2 +- .../geirolz/app/toolkit/logger/adapter.scala | 20 +++++++++++++++ .../geirolz/app/toolkit/logger/package.scala | 25 ------------------- .../geirolz/app/toolkit/logger/adapter.scala | 20 +++++++++++++++ .../geirolz/app/toolkit/logger/package.scala | 25 ------------------- .../logger/OdinLoggerAdapterSuite.scala | 8 +++--- 9 files changed, 60 insertions(+), 68 deletions(-) create mode 100644 integrations/log4cats/src/main/scala/com/geirolz/app/toolkit/logger/adapter.scala delete mode 100644 integrations/log4cats/src/main/scala/com/geirolz/app/toolkit/logger/package.scala create mode 100644 integrations/odin/src/main/scala/com/geirolz/app/toolkit/logger/adapter.scala delete mode 100644 integrations/odin/src/main/scala/com/geirolz/app/toolkit/logger/package.scala diff --git a/build.sbt b/build.sbt index 5beafa8..3953666 100644 --- a/build.sbt +++ b/build.sbt @@ -203,6 +203,7 @@ def scalacSettings(scalaVersion: String): Seq[String] = Seq( "-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 diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala index c718aeb..9adaa77 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala @@ -134,7 +134,7 @@ object AppBuilder: inline def withoutDependencies: AppBuilder.SelectProvide[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, NoDependencies] = dependsOn[NoDependencies, FAILURE](Resource.pure(NoDependencies.value)) - inline def dependsOn[DEPENDENCIES: ClassTag, FAILURE2 <: FAILURE: ClassTag]( + inline def dependsOn[DEPENDENCIES, FAILURE2 <: FAILURE: ClassTag]( f: AppContext[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 { 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 ade7779..1431bc7 100644 --- a/core/src/test/scala/com/geirolz/app/toolkit/AppSuite.scala +++ b/core/src/test/scala/com/geirolz/app/toolkit/AppSuite.scala @@ -5,6 +5,7 @@ import cats.effect.{IO, Ref, Resource} import com.geirolz.app.toolkit.App.ctx import com.geirolz.app.toolkit.failure.FailureHandler.OnFailureBehaviour import com.geirolz.app.toolkit.logger.Logger +import com.geirolz.app.toolkit.novalues.NoFailure import com.geirolz.app.toolkit.testing.* import scala.concurrent.duration.DurationInt @@ -23,7 +24,7 @@ class AppSuite extends munit.CatsEffectSuite { counter: Ref[IO, Int] <- IO.ref(0) _ <- App[IO] .withInfo(TestAppInfo.value) - .withPureLogger(Logger.console[IO](_)) + .withConsoleLogger() .withPureConfig(TestConfig.defaultTest) .dependsOn(Resource.pure[IO, Ref[IO, Int]](counter).trace(LabeledResource.appDependencies)) .provideOne(_.dependencies.set(1)) @@ -66,9 +67,9 @@ class AppSuite extends munit.CatsEffectSuite { for { _ <- App[IO] .withInfo(TestAppInfo.value) - .withPureLogger(Logger.console[IO](_)) + .withConsoleLogger() .withPureConfig(TestConfig.defaultTest) - .dependsOn(Resource.pure[IO, Unit](()).trace(LabeledResource.appDependencies)) + .dependsOn(Resource.unit[IO].trace(LabeledResource.appDependencies)) .provideOneF(_ => IO.raiseError(error"BOOM!")) .compile() .traceAsAppLoader @@ -102,7 +103,7 @@ class AppSuite extends munit.CatsEffectSuite { for { _ <- App[IO] .withInfo(TestAppInfo.value) - .withPureLogger(Logger.console[IO](_)) + .withConsoleLogger() .withPureConfig(TestConfig.defaultTest) .withoutDependencies .provide(_ => @@ -144,7 +145,7 @@ class AppSuite extends munit.CatsEffectSuite { for { _ <- App[IO] .withInfo(TestAppInfo.value) - .withPureLogger(Logger.console[IO](_)) + .withConsoleLogger() .withPureConfig(TestConfig.defaultTest) .withoutDependencies .provideOne(_ => IO.sleep(1.second)) @@ -180,7 +181,7 @@ class AppSuite extends munit.CatsEffectSuite { for { _ <- App[IO] .withInfo(TestAppInfo.value) - .withPureLogger(Logger.console[IO](_)) + .withConsoleLogger() .withPureConfig(TestConfig.defaultTest) .withoutDependencies .provideF(_ => @@ -226,7 +227,7 @@ class AppSuite extends munit.CatsEffectSuite { _ <- App[IO] .withInfo(TestAppInfo.value) - .withPureLogger(Logger.console[IO](_)) + .withConsoleLogger() .withPureConfig(TestConfig.defaultTest) .dependsOn(Resource.pure[IO, Unit](()).trace(LabeledResource.appDependencies)) .provideOne(_ => IO.raiseError(error"BOOM!")) @@ -266,7 +267,7 @@ class AppSuite extends munit.CatsEffectSuite { for { _ <- App[IO] .withInfo(TestAppInfo.value) - .withPureLogger(Logger.console[IO](_)) + .withConsoleLogger() .withPureConfig(TestConfig.defaultTest) .withoutDependencies .beforeProvidingSeq( @@ -327,7 +328,7 @@ class AppSuite extends munit.CatsEffectSuite { for { _ <- App[IO] .withInfo(TestAppInfo.value) - .withPureLogger(Logger.console[IO](_)) + .withConsoleLogger() .withPureConfig(TestConfig.defaultTest) .withoutDependencies .beforeProvidingSeq(_ => @@ -390,7 +391,7 @@ class AppSuite extends munit.CatsEffectSuite { state <- IO.ref[Boolean](false) _ <- App[IO] .withInfo(TestAppInfo.value) - .withPureLogger(Logger.console[IO](_)) + .withConsoleLogger() .withPureConfig(TestConfig.defaultTest) .withoutDependencies .provideOne(r => @@ -424,7 +425,7 @@ class AppSuite extends munit.CatsEffectSuite { state <- IO.ref[Boolean](false) app <- App[IO, AppError] .withInfo(TestAppInfo.value) - .withPureLogger(Logger.console[IO](_)) + .withConsoleLogger() .withPureConfig(TestConfig.defaultTest) .withoutDependencies .provideE(_ => @@ -462,7 +463,7 @@ class AppSuite extends munit.CatsEffectSuite { state <- IO.ref[Boolean](false) app <- App[IO, AppError] .withInfo(TestAppInfo.value) - .withPureLogger(Logger.console[IO](_)) + .withConsoleLogger() .withPureConfig(TestConfig.defaultTest) .withoutDependencies .provideE { _ => diff --git a/examples/src/main/scala/com/geirolz/example/app/AppMain.scala b/examples/src/main/scala/com/geirolz/example/app/AppMain.scala index 62bd76e..a87ab46 100644 --- a/examples/src/main/scala/com/geirolz/example/app/AppMain.scala +++ b/examples/src/main/scala/com/geirolz/example/app/AppMain.scala @@ -3,7 +3,7 @@ package com.geirolz.example.app import cats.effect.{ExitCode, IO, IOApp} import com.geirolz.app.toolkit.App import com.geirolz.app.toolkit.config.pureconfig.* -import com.geirolz.app.toolkit.logger.log4CatsLoggerAdapter +import com.geirolz.app.toolkit.logger.given import com.geirolz.app.toolkit.novalues.NoResources import com.geirolz.example.app.provided.AppHttpServer import org.typelevel.log4cats.SelfAwareStructuredLogger 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 2ef52b3..0000000 --- a/integrations/log4cats/src/main/scala/com/geirolz/app/toolkit/logger/package.scala +++ /dev/null @@ -1,25 +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]): 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/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 f42d7c2..0000000 --- a/integrations/odin/src/main/scala/com/geirolz/app/toolkit/logger/package.scala +++ /dev/null @@ -1,25 +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]): 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/test/scala/com/geirolz/app/toolkit/logger/OdinLoggerAdapterSuite.scala b/integrations/odin/src/test/scala/com/geirolz/app/toolkit/logger/OdinLoggerAdapterSuite.scala index 8e16b44..ac71c77 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,7 +18,7 @@ class OdinLoggerAdapterSuite extends munit.CatsEffectSuite { sbtVersion = "1.8.0" ) ) - .withPureLogger(Logger.noop[IO]) + .withPureLogger(OdinLogger.noop[IO]) .withoutDependencies .provideOne(_ => IO.unit) .run() @@ -27,8 +27,8 @@ class OdinLoggerAdapterSuite extends munit.CatsEffectSuite { } 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(error"BOOM!")("msg") From 6eb6d85953c5c1b19b7e33f53ea438e4abd1811c Mon Sep 17 00:00:00 2001 From: geirolz Date: Wed, 31 Jan 2024 14:14:10 +0100 Subject: [PATCH 16/22] Merge AppDependencies into AppContext --- README.md | 78 +++++++-------- .../scala/com/geirolz/app/toolkit/App.scala | 72 ++++++-------- .../com/geirolz/app/toolkit/AppBuilder.scala | 64 ++++++------- .../com/geirolz/app/toolkit/AppCompiler.scala | 21 +++-- .../com/geirolz/app/toolkit/AppContext.scala | 76 +++++++++++---- .../geirolz/app/toolkit/AppDependencies.scala | 44 --------- .../com/geirolz/app/toolkit/AppMessages.scala | 3 +- .../geirolz/app/toolkit/logger/Logger.scala | 2 +- .../app/toolkit/novalues/NoDependencies.scala | 2 +- .../scala/com/geirolz/app/toolkit/types.scala | 4 + .../app/toolkit/utils/ContextFunction.scala | 6 ++ .../AppContextAndDependenciesSuite.scala | 6 +- .../com/geirolz/app/toolkit/AppSuite.scala | 94 ++++--------------- docs/compiled/README.md | 76 +++++++-------- docs/compiled/integrations.md | 33 +++---- docs/source/README.md | 24 ++--- docs/source/integrations.md | 14 +-- .../example/app/AppDependencyServices.scala | 5 +- .../com/geirolz/example/app/AppMain.scala | 17 ++-- .../geirolz/example/app/AppWithFailures.scala | 13 +-- .../geirolz/app/toolkit/fly4s/package.scala | 59 ------------ .../com/geirolz/app/toolkit/fly4s/tasks.scala | 39 ++++++++ .../app/toolkit/fly4s/Fly4sSupportSuite.scala | 13 +-- .../logger/Log4CatsLoggerAdapterSuite.scala | 2 +- .../logger/OdinLoggerAdapterSuite.scala | 2 +- 25 files changed, 335 insertions(+), 434 deletions(-) delete mode 100644 core/src/main/scala/com/geirolz/app/toolkit/AppDependencies.scala create mode 100644 core/src/main/scala/com/geirolz/app/toolkit/utils/ContextFunction.scala delete mode 100644 integrations/fly4s/src/main/scala/com/geirolz/app/toolkit/fly4s/package.scala create mode 100644 integrations/fly4s/src/main/scala/com/geirolz/app/toolkit/fly4s/tasks.scala diff --git a/README.md b/README.md index 7570658..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: AppContext[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/core/src/main/scala/com/geirolz/app/toolkit/App.scala b/core/src/main/scala/com/geirolz/app/toolkit/App.scala index 272e222..fc31503 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/App.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/App.scala @@ -2,12 +2,12 @@ package com.geirolz.app.toolkit import cats.effect.* import cats.syntax.all.given -import cats.{Endo, Foldable, Parallel, Show} +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 import com.geirolz.app.toolkit.novalues.NoFailure.NotNoFailure +import com.geirolz.app.toolkit.novalues.NoFailure import scala.reflect.ClassTag @@ -25,28 +25,22 @@ class App[ val loggerBuilder: F[LOGGER_T[F]], val configLoader: Resource[F, CONFIG], val resourcesLoader: Resource[F, RESOURCES], - val beforeProvidingTask: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit], - val onFinalizeTask: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit], - val failureHandlerLoader: AppContext[INFO, LOGGER_T[F], CONFIG, RESOURCES] ?=> FailureHandler[F, FAILURE], - val depsLoader: AppContext[INFO, LOGGER_T[F], CONFIG, RESOURCES] ?=> Resource[F, FAILURE \/ DEPENDENCIES], - val servicesBuilder: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[FAILURE \/ List[F[FAILURE \/ Unit]]] + 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 Self = App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] - type Context = AppContext[INFO, LOGGER_T[F], CONFIG, RESOURCES] - - inline def onFinalizeSeq( - f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit], - fN: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit]* + type AppInfo = INFO + type Logger = LOGGER_T[F] + type Config = CONFIG + type Self = App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] + type ContextNoDeps = AppContext.NoDeps[INFO, LOGGER_T[F], CONFIG, RESOURCES] + + inline def onFinalize( + f: AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] ?=> F[Unit] ): Self = - onFinalizeSeq(deps => (f +: fN).map(_(deps))) - - inline def onFinalizeSeq[G[_]: Foldable]( - f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => G[F[Unit]] - ): Self = - copyWith(onFinalizeTask = d => this.onFinalizeTask(d) >> f(d).sequence_) + copyWith(onFinalizeTask = deps => this.onFinalizeTask(deps) >> f(using deps)) // compile and run inline def compile[R[_]]( @@ -74,16 +68,16 @@ class App[ RES2, DEPS2 ]( - 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: AppDependencies[APP_INFO2, LOGGER_T2[G], CONFIG2, DEPS2, RES2] => G[Unit] = this.beforeProvidingTask, - onFinalizeTask: AppDependencies[APP_INFO2, LOGGER_T2[G], CONFIG2, DEPS2, RES2] => G[Unit] = this.onFinalizeTask, - failureHandlerLoader: AppContext[APP_INFO2, LOGGER_T2[G], CONFIG2, RES2] ?=> FailureHandler[G, FAILURE2] = this.failureHandlerLoader, - dependenciesLoader: AppContext[APP_INFO2, LOGGER_T2[G], CONFIG2, RES2] ?=> Resource[G, FAILURE2 \/ DEPS2] = this.depsLoader, - provideBuilder: AppDependencies[APP_INFO2, LOGGER_T2[G], CONFIG2, DEPS2, RES2] => G[FAILURE2 \/ List[G[FAILURE2 \/ Unit]]] = this.servicesBuilder + 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]( info = appInfo, @@ -100,10 +94,6 @@ class App[ object App extends AppFailureSyntax: - inline def ctx[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, RESOURCES](using - c: AppContext[INFO, LOGGER, CONFIG, RESOURCES] - ): AppContext[INFO, LOGGER, CONFIG, RESOURCES] = c - inline def apply[F[+_]: Async: Parallel]: AppBuilder[F, NoFailure] = AppBuilder[F] @@ -124,7 +114,7 @@ sealed transparent trait AppFailureSyntax: // failures inline def mapFailure[FAILURE2]( - fhLoader: AppContext[INFO, LOGGER_T[F], CONFIG, RESOURCES] ?=> FailureHandler[F, FAILURE2] + fhLoader: AppContext.NoDeps[INFO, LOGGER_T[F], CONFIG, RESOURCES] ?=> FailureHandler[F, FAILURE2] )( f: FAILURE => FAILURE2 )(using NotNoFailure[FAILURE2]): App[F, FAILURE2, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = @@ -135,22 +125,22 @@ sealed transparent trait AppFailureSyntax: ) inline def onFailure_( - f: app.Context ?=> FAILURE => F[Unit] + f: app.ContextNoDeps ?=> FAILURE => F[Unit] ): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = _updateFailureHandlerLoader(_.onFailure(failure => f(failure) >> app.failureHandlerLoader.onFailureF(failure))) inline def onFailure( - f: app.Context ?=> FAILURE => F[OnFailureBehaviour] + f: app.ContextNoDeps ?=> FAILURE => F[OnFailureBehaviour] ): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = _updateFailureHandlerLoader(_.onFailure(f)) inline def handleFailureWith( - f: app.Context ?=> FAILURE => F[FAILURE \/ Unit] + f: app.ContextNoDeps ?=> FAILURE => F[FAILURE \/ Unit] ): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = _updateFailureHandlerLoader(_.handleFailureWith(f)) private def _updateFailureHandlerLoader( - fh: AppContext[INFO, LOGGER_T[F], CONFIG, RESOURCES] ?=> Endo[FailureHandler[F, FAILURE]] + fh: AppContext.NoDeps[INFO, LOGGER_T[F], CONFIG, RESOURCES] ?=> Endo[FailureHandler[F, FAILURE]] ): App[ F, FAILURE, diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala index 9adaa77..95d1d0c 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala @@ -2,7 +2,7 @@ package com.geirolz.app.toolkit import cats.effect.{Async, Resource} import cats.syntax.all.given -import cats.{Foldable, Parallel, Show} +import cats.{Parallel, Show} import com.geirolz.app.toolkit.App.* import com.geirolz.app.toolkit.AppBuilder.SelectResAndDeps import com.geirolz.app.toolkit.failure.FailureHandler @@ -135,7 +135,7 @@ object AppBuilder: dependsOn[NoDependencies, FAILURE](Resource.pure(NoDependencies.value)) inline def dependsOn[DEPENDENCIES, FAILURE2 <: FAILURE: ClassTag]( - f: AppContext[INFO, LOGGER_T[F], CONFIG, RESOURCES] ?=> Resource[F, FAILURE2 | DEPENDENCIES] + 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) @@ -143,7 +143,7 @@ object AppBuilder: }) def dependsOnE[DEPENDENCIES, FAILURE2 <: FAILURE]( - f: AppContext[INFO, LOGGER_T[F], CONFIG, RESOURCES] ?=> Resource[F, FAILURE2 \/ DEPENDENCIES] + 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, @@ -151,7 +151,7 @@ object AppBuilder: loggerBuilder = loggerBuilder, configLoader = configLoader, resourcesLoader = resourcesLoader, - dependenciesLoader = ctx => { given AppContext[INFO, LOGGER_T[F], CONFIG, RESOURCES] = ctx; f }, + dependenciesLoader = f(using _), beforeProvidingTask = _ => ().pure[F] ) @@ -190,74 +190,66 @@ object AppBuilder: loggerBuilder: F[LOGGER_T[F]], configLoader: Resource[F, CONFIG], resourcesLoader: Resource[F, RESOURCES], - dependenciesLoader: AppContext[INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, FAILURE \/ DEPENDENCIES], - beforeProvidingTask: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit] + 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 ------- - // TODO: Add failure - inline def beforeProvidingSeq( - f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit], - fN: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit]* + inline def beforeProviding( + f: AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] ?=> F[Unit] ): AppBuilder.SelectProvide[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = - beforeProvidingSeq(deps => (f +: fN).map(_(deps))) - - // TODO: Add failure - inline def beforeProvidingSeq[G[_]: Foldable]( - f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => G[F[Unit]] - ): AppBuilder.SelectProvide[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = - copy(beforeProvidingTask = d => this.beforeProvidingTask(d) >> f(d).sequence_) + copy(beforeProvidingTask = d => this.beforeProvidingTask(d) >> f(using d)) // ------- PROVIDE ------- def provideOne[FAILURE2 <: FAILURE: ClassTag]( - f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[FAILURE2 | Unit] + f: AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] ?=> F[FAILURE2 | Unit] ): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = - provideOneE[FAILURE2](f.andThen(_.map { + provideOneE[FAILURE2](f.map { case failure: FAILURE2 => Left(failure) case _: Unit => Right(()) - })) + }) inline def provideOneE[FAILURE2 <: FAILURE]( - f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[FAILURE2 \/ Unit] + f: AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] ?=> F[FAILURE2 \/ Unit] ): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = - provideE[FAILURE2](f.andThen(List(_))) + provideE[FAILURE2](List(f)) inline def provideOneF[FAILURE2 <: FAILURE]( - f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[FAILURE2 \/ F[Unit]] + f: AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] ?=> F[FAILURE2 \/ F[Unit]] ): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = - provideAttemptFE[FAILURE2](f.andThen(_.map(_.map(v => List(v.map(_.asRight[FAILURE2])))))) + provideAttemptFE[FAILURE2](f.map(_.map(v => List(v.map(_.asRight[FAILURE2]))))) // provide def provide[FAILURE2 <: FAILURE: ClassTag]( - f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => List[F[FAILURE2 | Unit]] + f: AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] ?=> List[F[FAILURE2 | Unit]] ): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = - provideE(f.andThen(_.map(_.map { + provideE(f.map(_.map { case failure: FAILURE2 => Left(failure) case _: Unit => Right(()) - }))) + })) inline def provideE[FAILURE2 <: FAILURE]( - f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => List[F[FAILURE2 \/ Unit]] + f: AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] ?=> List[F[FAILURE2 \/ Unit]] )(using DummyImplicit): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = - provideFE[FAILURE2](f.andThen(_.pure[F])) + provideFE[FAILURE2](f.pure[F]) // provideF def provideF[FAILURE2 <: FAILURE: ClassTag]( - f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[List[F[FAILURE2 | Unit]]] + 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] = - provideFE(f.andThen(_.map(_.map(_.map { + provideFE(f.map(_.map(_.map { case failure: FAILURE2 => Left(failure) case _: Unit => Right(()) - })))) + }))) inline def provideFE[FAILURE2 <: FAILURE]( - f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[List[F[FAILURE2 \/ Unit]]] + 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] = - provideAttemptFE(f.andThen(_.map(Right(_)))) + provideAttemptFE(f.map(Right(_))) // TODO Missing the union version def provideAttemptFE[FAILURE2 <: FAILURE]( - f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[FAILURE2 \/ List[F[FAILURE2 \/ Unit]]] + 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( @@ -273,5 +265,5 @@ object AppBuilder: onFinalizeTask = _ => ().pure[F], configLoader = configLoader, depsLoader = dependenciesLoader(ctx), - servicesBuilder = f + servicesBuilder = f(using _) ) diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppCompiler.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppCompiler.scala index b0c48db..95a8085 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppCompiler.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppCompiler.scala @@ -4,9 +4,9 @@ import cats.data.{EitherT, NonEmptyList} import cats.effect.implicits.{genSpawnOps, monadCancelOps_} import cats.effect.{Async, Fiber, Ref, Resource} import cats.{Parallel, Show} -import com.geirolz.app.toolkit.App.ctx import com.geirolz.app.toolkit.failure.FailureHandler.OnFailureBehaviour import com.geirolz.app.toolkit.logger.LoggerAdapter +import com.geirolz.app.toolkit.novalues.NoDependencies trait AppCompiler[F[+_]]: @@ -55,20 +55,21 @@ object AppCompiler: otherResources <- EitherT.right[FAILURE](app.resourcesLoader) // group resources - given AppContext[INFO, LOGGER_T[F], CONFIG, RESOURCES] = AppContext( - info = app.info, - messages = app.messages, - args = AppArgs(appArgs), - logger = userLogger, - config = appConfig, - resources = otherResources - ) + given AppContext.NoDeps[INFO, LOGGER_T[F], CONFIG, RESOURCES] = + AppContext.noDependencies( + info = app.info, + messages = app.messages, + args = AppArgs(appArgs), + logger = userLogger, + config = appConfig, + resources = otherResources + ) // ------------------- DEPENDENCIES ----------------- _ <- toolkitResLogger.debug(app.messages.buildingServicesEnv) appDepServices <- EitherT(app.depsLoader) _ <- toolkitResLogger.info(app.messages.servicesEnvSuccessfullyBuilt) - appDependencies = AppDependencies(ctx, appDepServices) + appDependencies = ctx.withDependencies(appDepServices) // --------------------- SERVICES ------------------- _ <- toolkitResLogger.debug(app.messages.buildingApp) diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppContext.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppContext.scala index d9e3242..2e711cd 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppContext.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppContext.scala @@ -1,19 +1,33 @@ 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, RESOURCES]( +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 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( @@ -21,37 +35,65 @@ final case class AppContext[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, RESOURCES] | args = $args, | logger = $logger, | config = $config, + | dependencies = $dependencies, | resources = $resources |)""".stripMargin } object AppContext: - private[toolkit] def apply[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, RESOURCES]( + 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, RESOURCES] = - new AppContext[INFO, LOGGER, CONFIG, RESOURCES]( - info = info, - messages = messages, - args = args, - logger = logger, - config = config, - 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, RESOURCES]( - res: AppContext[INFO, LOGGER, CONFIG, RESOURCES] - ): Option[(INFO, AppMessages, AppArgs, LOGGER, CONFIG, 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/AppDependencies.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppDependencies.scala deleted file mode 100644 index 729af34..0000000 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppDependencies.scala +++ /dev/null @@ -1,44 +0,0 @@ -package com.geirolz.app.toolkit - -import cats.syntax.all.given - -final case class AppDependencies[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, DEPENDENCIES, RESOURCES]( - private val _context: AppContext[INFO, LOGGER, CONFIG, RESOURCES], - private val _dependencies: DEPENDENCIES -): - - export _context.* - val dependencies: DEPENDENCIES = _dependencies - - override def toString: String = - s"""AppDependencies( - | info = $info, - | args = $args, - | logger = $logger, - | config = $config, - | resources = $resources, - | dependencies = $dependencies - |)""".stripMargin - -object AppDependencies: - - private[toolkit] def apply[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, DEPENDENCIES, RESOURCES]( - resources: AppContext[INFO, LOGGER, CONFIG, RESOURCES], - dependencies: DEPENDENCIES - ): AppDependencies[INFO, LOGGER, CONFIG, DEPENDENCIES, RESOURCES] = - new AppDependencies[INFO, LOGGER, CONFIG, DEPENDENCIES, RESOURCES]( - _context = resources, - _dependencies = dependencies - ) - - def unapply[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, DEPENDENCIES, RESOURCES]( - deps: AppDependencies[INFO, LOGGER, CONFIG, DEPENDENCIES, RESOURCES] - ): Option[(INFO, AppArgs, LOGGER, CONFIG, RESOURCES, DEPENDENCIES)] = - ( - deps.info, - deps.args, - deps.logger, - deps.config, - deps.resources, - deps.dependencies - ).some 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 15d98d0..d16e82c 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppMessages.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppMessages.scala @@ -11,7 +11,8 @@ case class AppMessages( appWasStopped: String, appAnErrorOccurred: String, appAFailureOccurred: String, - shuttingDownApp: String + shuttingDownApp: String, + unmapped: Map[String, String] = Map.empty ) object AppMessages: 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 index 5848675..1c2d0a5 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/logger/Logger.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/logger/Logger.scala @@ -1,7 +1,7 @@ package com.geirolz.app.toolkit.logger import cats.kernel.Order -import cats.{Show, ~>} +import cats.{~>, Show} trait Logger[F[_]]: def error(message: => String): F[Unit] 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 1074538..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 @@ -2,4 +2,4 @@ package com.geirolz.app.toolkit.novalues sealed trait NoDependencies object NoDependencies: - final val value: NoDependencies = new NoDependencies {} + private[toolkit] final val value: NoDependencies = new NoDependencies {} diff --git a/core/src/main/scala/com/geirolz/app/toolkit/types.scala b/core/src/main/scala/com/geirolz/app/toolkit/types.scala index 57c54b5..5ad57f9 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/types.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/types.scala @@ -4,3 +4,7 @@ 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/ContextFunction.scala b/core/src/main/scala/com/geirolz/app/toolkit/utils/ContextFunction.scala new file mode 100644 index 0000000..f1ec62e --- /dev/null +++ b/core/src/main/scala/com/geirolz/app/toolkit/utils/ContextFunction.scala @@ -0,0 +1,6 @@ +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 index 13accbd..0712cc1 100644 --- a/core/src/test/scala/com/geirolz/app/toolkit/AppContextAndDependenciesSuite.scala +++ b/core/src/test/scala/com/geirolz/app/toolkit/AppContextAndDependenciesSuite.scala @@ -14,7 +14,7 @@ class AppContextAndDependenciesSuite extends munit.FunSuite: .withPureConfig(TestConfig.defaultTest) .withoutResources .withoutDependencies - .provideOne(_ => IO.unit) + .provideOne(IO.unit) .run() .void } @@ -26,9 +26,7 @@ class AppContextAndDependenciesSuite extends munit.FunSuite: .withConsoleLogger() .withPureConfig(TestConfig.defaultTest) .withoutDependencies - .provideOne { case _ | AppDependencies(_, _, _, _, _, _) => - IO.unit - } + .provideOne(IO.unit) .run() .void } 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 1431bc7..77ef7ec 100644 --- a/core/src/test/scala/com/geirolz/app/toolkit/AppSuite.scala +++ b/core/src/test/scala/com/geirolz/app/toolkit/AppSuite.scala @@ -14,6 +14,7 @@ class AppSuite extends munit.CatsEffectSuite { import EventLogger.* import com.geirolz.app.toolkit.error.* + import cats.syntax.all.* test("Loader and App work as expected with dependsOn and logic fails") { EventLogger @@ -27,7 +28,7 @@ class AppSuite extends munit.CatsEffectSuite { .withConsoleLogger() .withPureConfig(TestConfig.defaultTest) .dependsOn(Resource.pure[IO, Ref[IO, Int]](counter).trace(LabeledResource.appDependencies)) - .provideOne(_.dependencies.set(1)) + .provideOne(ctx.dependencies.set(1)) .compile() .runFullTracedApp @@ -70,7 +71,7 @@ class AppSuite extends munit.CatsEffectSuite { .withConsoleLogger() .withPureConfig(TestConfig.defaultTest) .dependsOn(Resource.unit[IO].trace(LabeledResource.appDependencies)) - .provideOneF(_ => IO.raiseError(error"BOOM!")) + .provideOneF(IO.raiseError(error"BOOM!")) .compile() .traceAsAppLoader .attempt @@ -106,7 +107,7 @@ class AppSuite extends munit.CatsEffectSuite { .withConsoleLogger() .withPureConfig(TestConfig.defaultTest) .withoutDependencies - .provide(_ => + .provide( List( IO.sleep(300.millis), IO.sleep(50.millis), @@ -148,7 +149,7 @@ class AppSuite extends munit.CatsEffectSuite { .withConsoleLogger() .withPureConfig(TestConfig.defaultTest) .withoutDependencies - .provideOne(_ => IO.sleep(1.second)) + .provideOne(IO.sleep(1.second)) .compile() .runFullTracedApp @@ -184,7 +185,7 @@ class AppSuite extends munit.CatsEffectSuite { .withConsoleLogger() .withPureConfig(TestConfig.defaultTest) .withoutDependencies - .provideF(_ => + .provideF( IO( List( IO.sleep(300.millis), @@ -230,7 +231,7 @@ class AppSuite extends munit.CatsEffectSuite { .withConsoleLogger() .withPureConfig(TestConfig.defaultTest) .dependsOn(Resource.pure[IO, Unit](()).trace(LabeledResource.appDependencies)) - .provideOne(_ => IO.raiseError(error"BOOM!")) + .provideOne(IO.raiseError(error"BOOM!")) .compile() .runFullTracedApp .attempt @@ -259,68 +260,7 @@ class AppSuite extends munit.CatsEffectSuite { }) } - test("beforeProviding and onFinalizeSeq with varargs work as expected") { - EventLogger - .create[IO] - .flatMap(logger => { - implicit val loggerImplicit: EventLogger[IO] = logger - for { - _ <- App[IO] - .withInfo(TestAppInfo.value) - .withConsoleLogger() - .withPureConfig(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") { + test("beforeProviding and onFinalize with List work as expected") { EventLogger .create[IO] .flatMap(logger => { @@ -331,20 +271,20 @@ class AppSuite extends munit.CatsEffectSuite { .withConsoleLogger() .withPureConfig(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 @@ -394,9 +334,9 @@ class AppSuite extends munit.CatsEffectSuite { .withConsoleLogger() .withPureConfig(TestConfig.defaultTest) .withoutDependencies - .provideOne(r => + .provideOne( state.set( - r.args.exists( + ctx.args.exists( _.getVar[Int]("arg1").contains(1), _.hasFlags("verbose", "debug") ) @@ -428,7 +368,7 @@ class AppSuite extends munit.CatsEffectSuite { .withConsoleLogger() .withPureConfig(TestConfig.defaultTest) .withoutDependencies - .provideE(_ => + .provideE( List( IO(Left(AppError.Boom())), IO.sleep(1.seconds) >> IO(Left(AppError.Boom())), @@ -466,7 +406,7 @@ class AppSuite extends munit.CatsEffectSuite { .withConsoleLogger() .withPureConfig(TestConfig.defaultTest) .withoutDependencies - .provideE { _ => + .provideE { List( IO(Left(AppError.Boom())), IO.sleep(1.seconds) >> IO(Left(AppError.Boom())), diff --git a/docs/compiled/README.md b/docs/compiled/README.md index 3bdd826..572e337 100644 --- a/docs/compiled/README.md +++ b/docs/compiled/README.md @@ -70,33 +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.Logger +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: AppContext[SimpleAppInfo[String], Logger[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 @@ -107,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 @@ -115,35 +108,34 @@ object KafkaConsumer { ```scala import cats.effect.{ExitCode, IO, IOApp} -import com.geirolz.app.toolkit.{App, SimpleAppInfo} +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" +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 ) - ) - .withPureLogger(Logger.console[IO](_)) - .withConfigF(_ => 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 - ) - .onFinalizeSeq(_.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 b78f784..c290319 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( @@ -71,8 +71,9 @@ App[IO] ) .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( @@ -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 0fd4b2a..6f06e1c 100644 --- a/docs/source/README.md +++ b/docs/source/README.md @@ -70,8 +70,8 @@ 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.Logger +import com.geirolz.app.toolkit.* +import com.geirolz.app.toolkit.logger.ConsoleLogger import com.geirolz.app.toolkit.novalues.NoResources // Define config @@ -83,7 +83,7 @@ object Config: case class AppDependencyServices(kafkaConsumer: KafkaConsumer[IO]) object AppDependencyServices: - def resource(res: AppContext[SimpleAppInfo[String], Logger[IO], Config, NoResources]): Resource[IO, AppDependencyServices] = + def resource(using AppContext.NoDepsAndRes[SimpleAppInfo[String], ConsoleLogger[IO], Config]): Resource[IO, AppDependencyServices] = Resource.pure(AppDependencyServices(KafkaConsumer.fake)) // A stubbed kafka consumer @@ -108,7 +108,7 @@ object KafkaConsumer: ```scala mdoc:silent import cats.effect.{ExitCode, IO, IOApp} -import com.geirolz.app.toolkit.{App, SimpleAppInfo} +import com.geirolz.app.toolkit.* import com.geirolz.app.toolkit.logger.Logger object Main extends IOApp: @@ -122,19 +122,19 @@ object Main extends IOApp: sbtVersion = "1.8.0" ) ) - .withPureLogger(Logger.console[IO](_)) - .withConfigF(_ => IO.pure(Config("localhost", 8080))) - .dependsOn(AppDependencyServices.resource(_)) - .beforeProviding(_.logger.info("CUSTOM PRE-PROVIDING")) - .provideOne(deps => + .withConsoleLogger() + .withConfigF(IO.pure(Config("localhost", 8080))) + .dependsOn(AppDependencyServices.resource) + .beforeProviding(ctx.logger.info("CUSTOM PRE-PROVIDING")) + .provideOne( // Kafka consumer - deps.dependencies.kafkaConsumer + ctx.dependencies.kafkaConsumer .consumeFrom("test-topic") - .evalTap(record => deps.logger.info(s"Received record $record")) + .evalTap(record => ctx.logger.info(s"Received record $record")) .compile .drain ) - .onFinalizeSeq(_.logger.info("CUSTOM END")) + .onFinalize(ctx.logger.info("CUSTOM END")) .run(args) ``` diff --git a/docs/source/integrations.md b/docs/source/integrations.md index 2e629c2..31be0cb 100644 --- a/docs/source/integrations.md +++ b/docs/source/integrations.md @@ -71,7 +71,7 @@ App[IO] ) .withConfigF(pureconfigLoader[IO, TestConfig]) .withoutDependencies - .provideOne(_ => IO.unit) + .provideOne(IO.unit) .run() .void ``` @@ -156,7 +156,7 @@ 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]]) @@ -181,13 +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) + .provideOne(IO.unit) .run() .void ``` \ No newline at end of file diff --git a/examples/src/main/scala/com/geirolz/example/app/AppDependencyServices.scala b/examples/src/main/scala/com/geirolz/example/app/AppDependencyServices.scala index b183c16..0a85da7 100644 --- a/examples/src/main/scala/com/geirolz/example/app/AppDependencyServices.scala +++ b/examples/src/main/scala/com/geirolz/example/app/AppDependencyServices.scala @@ -2,7 +2,6 @@ package com.geirolz.example.app import cats.effect.{IO, Resource} import com.geirolz.app.toolkit.App.ctx -import com.geirolz.app.toolkit.novalues.NoResources import com.geirolz.app.toolkit.{App, AppContext} import com.geirolz.example.app.provided.KafkaConsumer import org.typelevel.log4cats.SelfAwareStructuredLogger @@ -12,7 +11,9 @@ case class AppDependencyServices( ) object AppDependencyServices: - def resource(using AppContext[AppInfo, SelfAwareStructuredLogger[IO], AppConfig, NoResources]): Resource[IO, AppDependencyServices] = + def resource(using + AppContext.NoDepsAndRes[AppInfo, SelfAwareStructuredLogger[IO], AppConfig] + ): Resource[IO, AppDependencyServices] = Resource.pure( AppDependencyServices( KafkaConsumer.fake(ctx.config.kafkaBroker.host) diff --git a/examples/src/main/scala/com/geirolz/example/app/AppMain.scala b/examples/src/main/scala/com/geirolz/example/app/AppMain.scala index a87ab46..bba9b00 100644 --- a/examples/src/main/scala/com/geirolz/example/app/AppMain.scala +++ b/examples/src/main/scala/com/geirolz/example/app/AppMain.scala @@ -1,13 +1,14 @@ package com.geirolz.example.app import cats.effect.{ExitCode, IO, IOApp} -import com.geirolz.app.toolkit.App +import com.geirolz.app.toolkit.* import com.geirolz.app.toolkit.config.pureconfig.* import com.geirolz.app.toolkit.logger.given import com.geirolz.app.toolkit.novalues.NoResources import com.geirolz.example.app.provided.AppHttpServer import org.typelevel.log4cats.SelfAwareStructuredLogger import org.typelevel.log4cats.slf4j.Slf4jLogger +import cats.syntax.all.given object AppMain extends IOApp: override def run(args: List[String]): IO[ExitCode] = @@ -15,20 +16,20 @@ object AppMain extends IOApp: .withInfo(AppInfo.fromBuildInfo) .withPureLogger(Slf4jLogger.getLogger[IO]) .withConfigF(pureconfigLoader[IO, AppConfig]) - .dependsOn(AppDependencyServices.resource) - .beforeProvidingSeq(_.logger.info("CUSTOM PRE-RUN")) - .provide(deps => + .dependsOnE(AppDependencyServices.resource.map(_.asRight)) + .beforeProviding(ctx.logger.info("CUSTOM PRE-RUN")) + .provide( List( // HTTP server - AppHttpServer.resource(deps.config).useForever, + AppHttpServer.resource(ctx.config).useForever, // Kafka consumer - deps.dependencies.kafkaConsumer + ctx.dependencies.kafkaConsumer .consumeFrom("test-topic") - .evalTap(record => deps.logger.info(s"Received record $record")) + .evalTap(record => ctx.logger.info(s"Received record $record")) .compile .drain ) ) - .onFinalizeSeq(_.logger.info("CUSTOM END")) + .onFinalize(ctx.logger.info("CUSTOM END")) .run(args) diff --git a/examples/src/main/scala/com/geirolz/example/app/AppWithFailures.scala b/examples/src/main/scala/com/geirolz/example/app/AppWithFailures.scala index 84a9e86..06d891c 100644 --- a/examples/src/main/scala/com/geirolz/example/app/AppWithFailures.scala +++ b/examples/src/main/scala/com/geirolz/example/app/AppWithFailures.scala @@ -1,6 +1,7 @@ package com.geirolz.example.app import cats.effect.{ExitCode, IO, IOApp} +import com.geirolz.app.toolkit.App.ctx import com.geirolz.app.toolkit.{App, AppMessages} import com.geirolz.app.toolkit.config.pureconfig.pureconfigLoader import com.geirolz.app.toolkit.logger.given @@ -15,19 +16,19 @@ object AppWithFailures extends IOApp: .withPureLogger(Slf4jLogger.getLogger[IO]) .withConfigF(pureconfigLoader[IO, AppConfig]) .dependsOn(AppDependencyServices.resource) - .beforeProvidingSeq(_.logger.info("CUSTOM PRE-RUN")) - .provide(deps => + .beforeProviding(ctx.logger.info("CUSTOM PRE-RUN")) + .provide( List( // HTTP server - AppHttpServer.resource(deps.config).useForever, + AppHttpServer.resource(ctx.config).useForever, // Kafka consumer - deps.dependencies.kafkaConsumer + ctx.dependencies.kafkaConsumer .consumeFrom("test-topic") - .evalTap(record => deps.logger.info(s"Received record $record")) + .evalTap(record => ctx.logger.info(s"Received record $record")) .compile .drain ) ) - .onFinalizeSeq(_.logger.info("CUSTOM END")) + .onFinalize(ctx.logger.info("CUSTOM END")) .run() 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 1651db0..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, 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 - ): AppDependencies[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, INFO <: SimpleAppInfo[?], LOGGER_T[_[_]]: LoggerAdapter, CONFIG, DEPENDENCIES, RESOURCES]( - url: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => String, - user: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => Option[String] = (_: Any) => None, - password: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => Option[Array[Char]] = (_: Any) => None, - config: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => Fly4sConfig = (_: Any) => Fly4sConfig.default, - classLoader: ClassLoader = Thread.currentThread.getContextClassLoader - ): AppDependencies[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, INFO <: SimpleAppInfo[?], LOGGER_T[_[_]]: LoggerAdapter, CONFIG, DEPENDENCIES, RESOURCES]( - f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => Resource[F, Fly4s[F]] - ): AppDependencies[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..ed281d8 --- /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 com.geirolz.app.toolkit.logger.LoggerAdapter +import cats.syntax.all.* +import com.geirolz.app.toolkit.* + +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 AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES]): 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 AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES]): F[Unit] = + fly4s + .evalMap(fl4s => + for { + logger <- LoggerAdapter[LOGGER_T].toToolkit(ctx.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/test/scala/com/geirolz/app/toolkit/fly4s/Fly4sSupportSuite.scala b/integrations/fly4s/src/test/scala/com/geirolz/app/toolkit/fly4s/Fly4sSupportSuite.scala index 8cf2315..7ca994e 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 @@ -3,6 +3,7 @@ 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.App.ctx class Fly4sSupportSuite extends munit.CatsEffectSuite { @@ -24,13 +25,13 @@ class Fly4sSupportSuite extends munit.CatsEffectSuite { ) ) .withoutDependencies - .beforeProvidingSeq( - migrateDatabaseWithConfig( - url = _.dbUrl, - user = _.dbUser, - password = _.dbPassword + .beforeProviding( + migrateDatabaseWith( + url = ctx.config.dbUrl, + user = ctx.config.dbUser, + password = ctx.config.dbPassword ) ) - .provideOne(_ => IO.unit) + .provideOne(IO.unit) } } 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 a126944..45b0f69 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 @@ -21,7 +21,7 @@ class Log4CatsLoggerAdapterSuite extends munit.CatsEffectSuite { ) .withPureLogger(NoOpLogger[IO]) .withoutDependencies - .provideOne(_ => IO.unit) + .provideOne(IO.unit) .run() .void ) 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 ac71c77..4659caf 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 @@ -20,7 +20,7 @@ class OdinLoggerAdapterSuite extends munit.CatsEffectSuite { ) .withPureLogger(OdinLogger.noop[IO]) .withoutDependencies - .provideOne(_ => IO.unit) + .provideOne(IO.unit) .run() .void ) From aafc06c1150d8ee85aba93c68d4eb608d0d27288 Mon Sep 17 00:00:00 2001 From: geirolz Date: Wed, 31 Jan 2024 15:44:37 +0100 Subject: [PATCH 17/22] Add IOApp entry point --- .../main/scala/com/geirolz/app/toolkit/App.scala | 3 +-- .../scala/com/geirolz/app/toolkit/IOApp.scala | 9 +++++++++ examples/buildinfo.properties | 4 ++-- .../example/app/AppDependencyServices.scala | 3 +-- .../scala/com/geirolz/example/app/AppMain.scala | 13 ++++++------- .../geirolz/example/app/AppWithFailures.scala | 16 ++++++++-------- 6 files changed, 27 insertions(+), 21 deletions(-) create mode 100644 core/src/main/scala/com/geirolz/app/toolkit/IOApp.scala 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 fc31503..8109b6e 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/App.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/App.scala @@ -34,12 +34,11 @@ class App[ type AppInfo = INFO type Logger = LOGGER_T[F] type Config = CONFIG - type Self = App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] type ContextNoDeps = AppContext.NoDeps[INFO, LOGGER_T[F], CONFIG, RESOURCES] inline def onFinalize( f: AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] ?=> F[Unit] - ): Self = + ): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] = copyWith(onFinalizeTask = deps => this.onFinalizeTask(deps) >> f(using deps)) // compile and run 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/examples/buildinfo.properties b/examples/buildinfo.properties index e4fbb35..da910d8 100644 --- a/examples/buildinfo.properties +++ b/examples/buildinfo.properties @@ -1,2 +1,2 @@ -#Tue Jan 30 18:16:36 CET 2024 -buildnumber=231 +#Wed Jan 31 15:21:18 CET 2024 +buildnumber=238 diff --git a/examples/src/main/scala/com/geirolz/example/app/AppDependencyServices.scala b/examples/src/main/scala/com/geirolz/example/app/AppDependencyServices.scala index 0a85da7..615a6d7 100644 --- a/examples/src/main/scala/com/geirolz/example/app/AppDependencyServices.scala +++ b/examples/src/main/scala/com/geirolz/example/app/AppDependencyServices.scala @@ -1,8 +1,7 @@ package com.geirolz.example.app import cats.effect.{IO, Resource} -import com.geirolz.app.toolkit.App.ctx -import com.geirolz.app.toolkit.{App, AppContext} +import com.geirolz.app.toolkit.{AppContext, ctx} import com.geirolz.example.app.provided.KafkaConsumer import org.typelevel.log4cats.SelfAwareStructuredLogger diff --git a/examples/src/main/scala/com/geirolz/example/app/AppMain.scala b/examples/src/main/scala/com/geirolz/example/app/AppMain.scala index bba9b00..41ce398 100644 --- a/examples/src/main/scala/com/geirolz/example/app/AppMain.scala +++ b/examples/src/main/scala/com/geirolz/example/app/AppMain.scala @@ -1,17 +1,17 @@ package com.geirolz.example.app -import cats.effect.{ExitCode, IO, IOApp} -import com.geirolz.app.toolkit.* +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.NoResources +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 -import cats.syntax.all.given -object AppMain extends IOApp: - override def run(args: List[String]): IO[ExitCode] = +object AppMain extends IOApp.Toolkit: + val app: App[IO, NoFailure, AppInfo, SelfAwareStructuredLogger, AppConfig, NoResources, AppDependencyServices] = App[IO] .withInfo(AppInfo.fromBuildInfo) .withPureLogger(Slf4jLogger.getLogger[IO]) @@ -32,4 +32,3 @@ object AppMain extends IOApp: ) ) .onFinalize(ctx.logger.info("CUSTOM END")) - .run(args) diff --git a/examples/src/main/scala/com/geirolz/example/app/AppWithFailures.scala b/examples/src/main/scala/com/geirolz/example/app/AppWithFailures.scala index 06d891c..53bdbb9 100644 --- a/examples/src/main/scala/com/geirolz/example/app/AppWithFailures.scala +++ b/examples/src/main/scala/com/geirolz/example/app/AppWithFailures.scala @@ -1,21 +1,22 @@ package com.geirolz.example.app -import cats.effect.{ExitCode, IO, IOApp} -import com.geirolz.app.toolkit.App.ctx -import com.geirolz.app.toolkit.{App, AppMessages} +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: - - override def run(args: List[String]): IO[ExitCode] = +object AppWithFailures extends IOApp.Toolkit: + val app: App[IO, AppError, AppInfo, SelfAwareStructuredLogger, AppConfig, NoResources, AppDependencyServices] = App[IO, AppError] .withInfo(AppInfo.fromBuildInfo) .withPureLogger(Slf4jLogger.getLogger[IO]) .withConfigF(pureconfigLoader[IO, AppConfig]) - .dependsOn(AppDependencyServices.resource) + .dependsOnE(AppDependencyServices.resource.map(_.asRight)) .beforeProviding(ctx.logger.info("CUSTOM PRE-RUN")) .provide( List( @@ -31,4 +32,3 @@ object AppWithFailures extends IOApp: ) ) .onFinalize(ctx.logger.info("CUSTOM END")) - .run() From a344c6ee5f80f110f748b949c89bfe2641512ec8 Mon Sep 17 00:00:00 2001 From: geirolz Date: Wed, 31 Jan 2024 18:50:53 +0100 Subject: [PATCH 18/22] Resources are released once context is built --- .../com/geirolz/app/toolkit/AppBuilder.scala | 40 ++++++----- .../com/geirolz/app/toolkit/AppCompiler.scala | 38 +++++----- .../com/geirolz/app/toolkit/AppMessages.scala | 6 +- .../AppContextAndDependenciesSuite.scala | 4 +- .../com/geirolz/app/toolkit/AppSuite.scala | 71 +++++++++++++++---- examples/buildinfo.properties | 4 +- .../com/geirolz/example/app/AppMain.scala | 2 +- .../geirolz/example/app/AppWithFailures.scala | 2 +- .../app/toolkit/fly4s/Fly4sAppMessages.scala | 11 +++ .../com/geirolz/app/toolkit/fly4s/tasks.scala | 12 ++-- .../app/toolkit/fly4s/Fly4sSupportSuite.scala | 7 +- .../toolkit/fly4s/testing/TestConfig.scala | 5 +- .../logger/Log4CatsLoggerAdapterSuite.scala | 2 +- .../logger/OdinLoggerAdapterSuite.scala | 2 +- .../geirolz/app/toolkit/testing/events.scala | 1 + 15 files changed, 137 insertions(+), 70 deletions(-) create mode 100644 integrations/fly4s/src/main/scala/com/geirolz/app/toolkit/fly4s/Fly4sAppMessages.scala diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala index 95d1d0c..c50f0dd 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala @@ -2,7 +2,8 @@ package com.geirolz.app.toolkit import cats.effect.{Async, Resource} import cats.syntax.all.given -import cats.{Parallel, Show} +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 @@ -51,21 +52,24 @@ object AppBuilder: // ------- MESSAGES ------- inline def withMessages(messages: AppMessages): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES] = - copyWith(messages = messages) + 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] = - withPureLogger(logger = NoopLogger[F]) + withLogger(logger = NoopLogger[F]) inline def withConsoleLogger(minLevel: Level = Level.Info): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, ConsoleLogger, CONFIG, RESOURCES] = - withPureLogger(logger = ConsoleLogger[F](info, minLevel)) + withLogger(logger = ConsoleLogger[F](info, minLevel)) - inline def withPureLogger[LOGGER_T2[_[_]]: LoggerAdapter]( + inline def withLogger[LOGGER_T2[_[_]]: LoggerAdapter]( logger: LOGGER_T2[F] ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T2, CONFIG, RESOURCES] = - withPureLogger[LOGGER_T2](f = (_: INFO) => logger) + withLogger[LOGGER_T2](f = (_: INFO) => logger) - inline def withPureLogger[LOGGER_T2[_[_]]: LoggerAdapter]( + inline def withLogger[LOGGER_T2[_[_]]: LoggerAdapter]( f: INFO => LOGGER_T2[F] ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T2, CONFIG, RESOURCES] = withLoggerF(f = appInfo => f(appInfo).pure[F]) @@ -78,9 +82,9 @@ object AppBuilder: // ------- CONFIG ------- inline def withoutConfig: AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, NoConfig, RESOURCES] = - withPureConfig[NoConfig](NoConfig.value) + withConfig[NoConfig](NoConfig.value) - inline def withPureConfig[CONFIG2: Show]( + inline def withConfig[CONFIG2: Show]( config: CONFIG2 ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG2, RESOURCES] = withConfigF(config.pure[F]) @@ -89,31 +93,31 @@ object AppBuilder: 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))) + withConfigResource(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)) + withConfigResource(Resource.eval(configLoader)) // TODO: Add failure - inline def withConfig[CONFIG2: Show]( + inline def withConfigResource[CONFIG2: Show]( configLoader: Resource[F, CONFIG2] ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG2, RESOURCES] = - withConfig(_ => configLoader) + withConfigResource(_ => configLoader) // TODO: Add failure - inline def withConfig[CONFIG2: Show]( + inline def withConfigResource[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] = - withPureResources[NoResources](NoResources.value) + withResources[NoResources](NoResources.value) - inline def withPureResources[RESOURCES2]( + inline def withResources[RESOURCES2]( resources: RESOURCES2 ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES2] = withResourcesF(resources.pure[F]) @@ -122,10 +126,10 @@ object AppBuilder: inline def withResourcesF[RESOURCES2]( resourcesLoader: F[RESOURCES2] ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES2] = - withResources(Resource.eval(resourcesLoader)) + withResourcesResource(Resource.eval(resourcesLoader)) // TODO: Add failure - inline def withResources[RESOURCES2]( + inline def withResourcesResource[RESOURCES2]( resourcesLoader: Resource[F, RESOURCES2] ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES2] = copyWith(resourcesLoader = resourcesLoader) diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppCompiler.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppCompiler.scala index 95a8085..01f1c77 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppCompiler.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppCompiler.scala @@ -4,6 +4,7 @@ import cats.data.{EitherT, NonEmptyList} import cats.effect.implicits.{genSpawnOps, monadCancelOps_} import cats.effect.{Async, Fiber, Ref, Resource} 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 @@ -37,7 +38,7 @@ object AppCompiler: ( for { - // -------------------- RESOURCES------------------- + // -------------------- CONTEXT ------------------- // logger userLogger <- EitherT.right[FAILURE](Resource.eval(app.loggerBuilder)) toolkitLogger = LoggerAdapter[LOGGER_T].toToolkit[F](userLogger) @@ -51,29 +52,34 @@ object AppCompiler: _ <- toolkitResLogger.info(app.messages.configSuccessfullyLoaded) _ <- toolkitResLogger.info(appConfig.show) - // other resources - otherResources <- EitherT.right[FAILURE](app.resourcesLoader) - // group resources - given AppContext.NoDeps[INFO, LOGGER_T[F], CONFIG, RESOURCES] = - AppContext.noDependencies( - info = app.info, - messages = app.messages, - 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.messages.buildingServicesEnv) appDepServices <- EitherT(app.depsLoader) _ <- toolkitResLogger.info(app.messages.servicesEnvSuccessfullyBuilt) - appDependencies = ctx.withDependencies(appDepServices) + appContext = ctx.withDependencies(appDepServices) // --------------------- SERVICES ------------------- _ <- toolkitResLogger.debug(app.messages.buildingApp) - appProvServices <- EitherT(Resource.eval(app.servicesBuilder(appDependencies))) + appProvServices <- EitherT(Resource.eval(app.servicesBuilder(appContext))) _ <- toolkitResLogger.info(app.messages.appSuccessfullyBuilt) // --------------------- APP ------------------------ @@ -111,12 +117,12 @@ object AppCompiler: } yield maybeReducedFailures.toLeft(()) } yield { toolkitLogger.info(app.messages.startingApp) >> - app.beforeProvidingTask(appDependencies) >> + app.beforeProvidingTask(appContext) >> appLogic .onCancel(toolkitLogger.info(app.messages.appWasStopped)) .onError(e => toolkitLogger.error(e)(app.messages.appAnErrorOccurred)) .guarantee( - app.onFinalizeTask(appDependencies) >> toolkitLogger.info(app.messages.shuttingDownApp) + app.onFinalizeTask(appContext) >> toolkitLogger.info(app.messages.shuttingDownApp) ) } ).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 d16e82c..c92ee87 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppMessages.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppMessages.scala @@ -11,9 +11,9 @@ case class AppMessages( appWasStopped: String, appAnErrorOccurred: String, appAFailureOccurred: String, - shuttingDownApp: String, - unmapped: Map[String, String] = Map.empty + shuttingDownApp: String ) + object AppMessages: inline def fromAppInfo[INFO <: SimpleAppInfo[?]](info: INFO)( @@ -21,7 +21,7 @@ object AppMessages: ): AppMessages = f(info) def default(info: SimpleAppInfo[?]): AppMessages = - AppMessages.fromAppInfo(info)(info => + fromAppInfo(info)(info => AppMessages( loadingConfig = "Loading configuration...", configSuccessfullyLoaded = "Configuration successfully loaded.", diff --git a/core/src/test/scala/com/geirolz/app/toolkit/AppContextAndDependenciesSuite.scala b/core/src/test/scala/com/geirolz/app/toolkit/AppContextAndDependenciesSuite.scala index 0712cc1..7e71ed3 100644 --- a/core/src/test/scala/com/geirolz/app/toolkit/AppContextAndDependenciesSuite.scala +++ b/core/src/test/scala/com/geirolz/app/toolkit/AppContextAndDependenciesSuite.scala @@ -11,7 +11,7 @@ class AppContextAndDependenciesSuite extends munit.FunSuite: val res = App[IO] .withInfo(TestAppInfo.value) .withConsoleLogger() - .withPureConfig(TestConfig.defaultTest) + .withConfig(TestConfig.defaultTest) .withoutResources .withoutDependencies .provideOne(IO.unit) @@ -24,7 +24,7 @@ class AppContextAndDependenciesSuite extends munit.FunSuite: App[IO] .withInfo(TestAppInfo.value) .withConsoleLogger() - .withPureConfig(TestConfig.defaultTest) + .withConfig(TestConfig.defaultTest) .withoutDependencies .provideOne(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 77ef7ec..77f1458 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,8 @@ package com.geirolz.app.toolkit import cats.data.NonEmptyList import cats.effect.{IO, Ref, Resource} -import com.geirolz.app.toolkit.App.ctx +import com.geirolz.app.toolkit.* import com.geirolz.app.toolkit.failure.FailureHandler.OnFailureBehaviour -import com.geirolz.app.toolkit.logger.Logger import com.geirolz.app.toolkit.novalues.NoFailure import com.geirolz.app.toolkit.testing.* @@ -16,6 +15,54 @@ class AppSuite extends munit.CatsEffectSuite { 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() + .withConfig(TestConfig.defaultTest) + .withResourcesResource(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 .create[IO] @@ -26,7 +73,7 @@ class AppSuite extends munit.CatsEffectSuite { _ <- App[IO] .withInfo(TestAppInfo.value) .withConsoleLogger() - .withPureConfig(TestConfig.defaultTest) + .withConfig(TestConfig.defaultTest) .dependsOn(Resource.pure[IO, Ref[IO, Int]](counter).trace(LabeledResource.appDependencies)) .provideOne(ctx.dependencies.set(1)) .compile() @@ -69,7 +116,7 @@ class AppSuite extends munit.CatsEffectSuite { _ <- App[IO] .withInfo(TestAppInfo.value) .withConsoleLogger() - .withPureConfig(TestConfig.defaultTest) + .withConfig(TestConfig.defaultTest) .dependsOn(Resource.unit[IO].trace(LabeledResource.appDependencies)) .provideOneF(IO.raiseError(error"BOOM!")) .compile() @@ -105,7 +152,7 @@ class AppSuite extends munit.CatsEffectSuite { _ <- App[IO] .withInfo(TestAppInfo.value) .withConsoleLogger() - .withPureConfig(TestConfig.defaultTest) + .withConfig(TestConfig.defaultTest) .withoutDependencies .provide( List( @@ -147,7 +194,7 @@ class AppSuite extends munit.CatsEffectSuite { _ <- App[IO] .withInfo(TestAppInfo.value) .withConsoleLogger() - .withPureConfig(TestConfig.defaultTest) + .withConfig(TestConfig.defaultTest) .withoutDependencies .provideOne(IO.sleep(1.second)) .compile() @@ -183,7 +230,7 @@ class AppSuite extends munit.CatsEffectSuite { _ <- App[IO] .withInfo(TestAppInfo.value) .withConsoleLogger() - .withPureConfig(TestConfig.defaultTest) + .withConfig(TestConfig.defaultTest) .withoutDependencies .provideF( IO( @@ -229,7 +276,7 @@ class AppSuite extends munit.CatsEffectSuite { App[IO] .withInfo(TestAppInfo.value) .withConsoleLogger() - .withPureConfig(TestConfig.defaultTest) + .withConfig(TestConfig.defaultTest) .dependsOn(Resource.pure[IO, Unit](()).trace(LabeledResource.appDependencies)) .provideOne(IO.raiseError(error"BOOM!")) .compile() @@ -269,7 +316,7 @@ class AppSuite extends munit.CatsEffectSuite { _ <- App[IO] .withInfo(TestAppInfo.value) .withConsoleLogger() - .withPureConfig(TestConfig.defaultTest) + .withConfig(TestConfig.defaultTest) .withoutDependencies .beforeProviding( List( @@ -332,7 +379,7 @@ class AppSuite extends munit.CatsEffectSuite { _ <- App[IO] .withInfo(TestAppInfo.value) .withConsoleLogger() - .withPureConfig(TestConfig.defaultTest) + .withConfig(TestConfig.defaultTest) .withoutDependencies .provideOne( state.set( @@ -366,7 +413,7 @@ class AppSuite extends munit.CatsEffectSuite { app <- App[IO, AppError] .withInfo(TestAppInfo.value) .withConsoleLogger() - .withPureConfig(TestConfig.defaultTest) + .withConfig(TestConfig.defaultTest) .withoutDependencies .provideE( List( @@ -404,7 +451,7 @@ class AppSuite extends munit.CatsEffectSuite { app <- App[IO, AppError] .withInfo(TestAppInfo.value) .withConsoleLogger() - .withPureConfig(TestConfig.defaultTest) + .withConfig(TestConfig.defaultTest) .withoutDependencies .provideE { List( diff --git a/examples/buildinfo.properties b/examples/buildinfo.properties index da910d8..ca931e9 100644 --- a/examples/buildinfo.properties +++ b/examples/buildinfo.properties @@ -1,2 +1,2 @@ -#Wed Jan 31 15:21:18 CET 2024 -buildnumber=238 +#Wed Jan 31 17:11:12 CET 2024 +buildnumber=240 diff --git a/examples/src/main/scala/com/geirolz/example/app/AppMain.scala b/examples/src/main/scala/com/geirolz/example/app/AppMain.scala index 41ce398..45d5c45 100644 --- a/examples/src/main/scala/com/geirolz/example/app/AppMain.scala +++ b/examples/src/main/scala/com/geirolz/example/app/AppMain.scala @@ -14,7 +14,7 @@ object AppMain extends IOApp.Toolkit: val app: App[IO, NoFailure, AppInfo, SelfAwareStructuredLogger, AppConfig, NoResources, AppDependencyServices] = App[IO] .withInfo(AppInfo.fromBuildInfo) - .withPureLogger(Slf4jLogger.getLogger[IO]) + .withLogger(Slf4jLogger.getLogger[IO]) .withConfigF(pureconfigLoader[IO, AppConfig]) .dependsOnE(AppDependencyServices.resource.map(_.asRight)) .beforeProviding(ctx.logger.info("CUSTOM PRE-RUN")) diff --git a/examples/src/main/scala/com/geirolz/example/app/AppWithFailures.scala b/examples/src/main/scala/com/geirolz/example/app/AppWithFailures.scala index 53bdbb9..c5ea103 100644 --- a/examples/src/main/scala/com/geirolz/example/app/AppWithFailures.scala +++ b/examples/src/main/scala/com/geirolz/example/app/AppWithFailures.scala @@ -14,7 +14,7 @@ object AppWithFailures extends IOApp.Toolkit: val app: App[IO, AppError, AppInfo, SelfAwareStructuredLogger, AppConfig, NoResources, AppDependencyServices] = App[IO, AppError] .withInfo(AppInfo.fromBuildInfo) - .withPureLogger(Slf4jLogger.getLogger[IO]) + .withLogger(Slf4jLogger.getLogger[IO]) .withConfigF(pureconfigLoader[IO, AppConfig]) .dependsOnE(AppDependencyServices.resource.map(_.asRight)) .beforeProviding(ctx.logger.info("CUSTOM PRE-RUN")) 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/tasks.scala b/integrations/fly4s/src/main/scala/com/geirolz/app/toolkit/fly4s/tasks.scala index ed281d8..9f9fa82 100644 --- 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 @@ -3,9 +3,9 @@ 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 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, @@ -13,7 +13,7 @@ def migrateDatabaseWith[F[_]: Async, INFO <: SimpleAppInfo[?], LOGGER_T[_[_]]: L password: Option[Array[Char]] = None, config: Fly4sConfig = Fly4sConfig.default, classLoader: ClassLoader = Thread.currentThread.getContextClassLoader -)(using AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES]): F[Unit] = +)(using c: AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES], msgs: Fly4sAppMessages): F[Unit] = migrateDatabase( Fly4s.make[F]( url = url, @@ -26,14 +26,14 @@ def migrateDatabaseWith[F[_]: Async, INFO <: SimpleAppInfo[?], LOGGER_T[_[_]]: L def migrateDatabase[F[_]: Async, INFO <: SimpleAppInfo[?], LOGGER_T[_[_]]: LoggerAdapter, CONFIG, DEPENDENCIES, RESOURCES]( fly4s: Resource[F, Fly4s[F]] -)(using AppContext[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES]): F[Unit] = +)(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(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.") + _ <- 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 7ca994e..fed25db 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,10 +2,10 @@ 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.{App, AppMessages, SimpleAppInfo} import com.geirolz.app.toolkit.App.ctx -class Fly4sSupportSuite extends munit.CatsEffectSuite { +class Fly4sSupportSuite extends munit.CatsEffectSuite: test("Syntax works as expected") { App[IO] @@ -17,7 +17,7 @@ class Fly4sSupportSuite extends munit.CatsEffectSuite { sbtVersion = "1.8.0" ) ) - .withPureConfig( + .withConfig( TestConfig( dbUrl = "jdbc:postgresql://localhost:5432/toolkit", dbUser = Some("postgres"), @@ -34,4 +34,3 @@ class Fly4sSupportSuite extends munit.CatsEffectSuite { ) .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/test/scala/com/geirolz/app/toolkit/logger/Log4CatsLoggerAdapterSuite.scala b/integrations/log4cats/src/test/scala/com/geirolz/app/toolkit/logger/Log4CatsLoggerAdapterSuite.scala index 45b0f69..ef8fb79 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,7 +19,7 @@ class Log4CatsLoggerAdapterSuite extends munit.CatsEffectSuite { sbtVersion = "1.8.0" ) ) - .withPureLogger(NoOpLogger[IO]) + .withLogger(NoOpLogger[IO]) .withoutDependencies .provideOne(IO.unit) .run() 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 4659caf..4983842 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 @@ -18,7 +18,7 @@ class OdinLoggerAdapterSuite extends munit.CatsEffectSuite { sbtVersion = "1.8.0" ) ) - .withPureLogger(OdinLogger.noop[IO]) + .withLogger(OdinLogger.noop[IO]) .withoutDependencies .provideOne(IO.unit) .run() 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 4fa3ec1..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 @@ -40,4 +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") From 043a97722afa689783bd189ca9259832dec61f9a Mon Sep 17 00:00:00 2001 From: geirolz Date: Thu, 1 Feb 2024 10:09:32 +0100 Subject: [PATCH 19/22] Fix naming and extends examples --- .../scala/com/geirolz/app/toolkit/App.scala | 9 ++- .../com/geirolz/app/toolkit/AppBuilder.scala | 73 +++++++++---------- .../geirolz/app/toolkit/logger/Logger.scala | 3 + .../AppContextAndDependenciesSuite.scala | 4 +- .../com/geirolz/app/toolkit/AppSuite.scala | 32 ++++---- examples/buildinfo.properties | 4 +- examples/src/main/resources/document.xml | 1 - examples/src/main/resources/host-table.txt | 1 + .../example/app/AppDependencyServices.scala | 12 +-- .../com/geirolz/example/app/AppMain.scala | 7 +- .../geirolz/example/app/AppResources.scala | 17 +++++ .../geirolz/example/app/AppWithFailures.scala | 7 +- .../example/app/provided/HostTable.scala | 17 +++++ .../app/toolkit/fly4s/Fly4sSupportSuite.scala | 5 +- .../logger/Log4CatsLoggerAdapterSuite.scala | 2 +- .../logger/OdinLoggerAdapterSuite.scala | 2 +- 16 files changed, 117 insertions(+), 79 deletions(-) delete mode 100644 examples/src/main/resources/document.xml create mode 100644 examples/src/main/resources/host-table.txt create mode 100644 examples/src/main/scala/com/geirolz/example/app/AppResources.scala create mode 100644 examples/src/main/scala/com/geirolz/example/app/provided/HostTable.scala 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 8109b6e..502c07c 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/App.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/App.scala @@ -93,11 +93,14 @@ class App[ object App extends AppFailureSyntax: - inline def apply[F[+_]: Async: Parallel]: AppBuilder[F, NoFailure] = - AppBuilder[F] + type Simple[F[+_], INFO <: SimpleAppInfo[?], LOGGER_T[_[_]], CONFIG, RESOURCES, DEPENDENCIES] = + App[F, NoFailure, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] + + inline def apply[F[+_]: Async: Parallel]: AppBuilder.Simple[F] = + AppBuilder.simple[F] inline def apply[F[+_]: Async: Parallel, FAILURE: ClassTag]: AppBuilder[F, FAILURE] = - AppBuilder[F, FAILURE] + AppBuilder.withFailure[F, FAILURE] sealed transparent trait AppFailureSyntax: diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala index c50f0dd..6a85170 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala @@ -29,10 +29,12 @@ final class AppBuilder[F[+_]: Async: Parallel, FAILURE: ClassTag]: object AppBuilder: - inline def apply[F[+_]: Async: Parallel]: AppBuilder[F, NoFailure] = + type Simple[F[+_]] = AppBuilder[F, NoFailure] + + inline def simple[F[+_]: Async: Parallel]: AppBuilder.Simple[F] = new AppBuilder[F, NoFailure] - inline def apply[F[+_]: Async: Parallel, FAILURE: ClassTag: NotNoFailure]: AppBuilder[F, FAILURE] = + inline def withFailure[F[+_]: Async: Parallel, FAILURE: ClassTag: NotNoFailure]: AppBuilder[F, FAILURE] = new AppBuilder[F, FAILURE] final class SelectResAndDeps[ @@ -59,32 +61,32 @@ object AppBuilder: // ------- LOGGER ------- inline def withNoopLogger: AppBuilder.SelectResAndDeps[F, FAILURE, INFO, NoopLogger, CONFIG, RESOURCES] = - withLogger(logger = NoopLogger[F]) + withLoggerPure(logger = Logger.noop[F]) inline def withConsoleLogger(minLevel: Level = Level.Info): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, ConsoleLogger, CONFIG, RESOURCES] = - withLogger(logger = ConsoleLogger[F](info, minLevel)) + withLoggerPure(logger = ConsoleLogger[F](info, minLevel)) - inline def withLogger[LOGGER_T2[_[_]]: LoggerAdapter]( + inline def withLoggerPure[LOGGER_T2[_[_]]: LoggerAdapter]( logger: LOGGER_T2[F] ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T2, CONFIG, RESOURCES] = - withLogger[LOGGER_T2](f = (_: INFO) => logger) + withLoggerPure[LOGGER_T2](f = (_: INFO) => logger) - inline def withLogger[LOGGER_T2[_[_]]: LoggerAdapter]( + inline def withLoggerPure[LOGGER_T2[_[_]]: LoggerAdapter]( f: INFO => LOGGER_T2[F] ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T2, CONFIG, RESOURCES] = - withLoggerF(f = appInfo => f(appInfo).pure[F]) + withLogger(f = appInfo => f(appInfo).pure[F]) // TODO: Add failure - inline def withLoggerF[LOGGER_T2[_[_]]: LoggerAdapter]( + 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] = - withConfig[NoConfig](NoConfig.value) + withConfigPure[NoConfig](NoConfig.value) - inline def withConfig[CONFIG2: Show]( + inline def withConfigPure[CONFIG2: Show]( config: CONFIG2 ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG2, RESOURCES] = withConfigF(config.pure[F]) @@ -93,43 +95,34 @@ object AppBuilder: inline def withConfigF[CONFIG2: Show]( configLoader: INFO => F[CONFIG2] )(using DummyImplicit): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG2, RESOURCES] = - withConfigResource(i => Resource.eval(configLoader(i))) + 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] = - withConfigResource(Resource.eval(configLoader)) + withConfig(Resource.eval(configLoader)) // TODO: Add failure - inline def withConfigResource[CONFIG2: Show]( + inline def withConfig[CONFIG2: Show]( configLoader: Resource[F, CONFIG2] ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG2, RESOURCES] = - withConfigResource(_ => configLoader) + withConfig(_ => configLoader) // TODO: Add failure - inline def withConfigResource[CONFIG2: Show]( + 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](NoResources.value) - - inline def withResources[RESOURCES2]( - resources: RESOURCES2 - ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES2] = - withResourcesF(resources.pure[F]) + withResources[NoResources](Resource.pure(NoResources.value)) // TODO: Add failure - inline def withResourcesF[RESOURCES2]( - resourcesLoader: F[RESOURCES2] - ): AppBuilder.SelectResAndDeps[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES2] = - withResourcesResource(Resource.eval(resourcesLoader)) - // TODO: Add failure - inline def withResourcesResource[RESOURCES2]( + /** 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) @@ -138,6 +131,7 @@ object AppBuilder: 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 he 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] = @@ -146,6 +140,7 @@ object AppBuilder: case failure: FAILURE2 => Left(failure) }) + /** Dependencies are loaded into context and released at he 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] = @@ -216,43 +211,43 @@ object AppBuilder: 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] = - provideE[FAILURE2](List(f)) + 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] = - provideAttemptFE[FAILURE2](f.map(_.map(v => List(v.map(_.asRight[FAILURE2]))))) + provideParallelAttemptFE[FAILURE2](f.map(_.map(v => List(v.map(_.asRight[FAILURE2]))))) // provide - def provide[FAILURE2 <: FAILURE: ClassTag]( + 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] = - provideE(f.map(_.map { + provideParallelE(f.map(_.map { case failure: FAILURE2 => Left(failure) case _: Unit => Right(()) })) - inline def provideE[FAILURE2 <: FAILURE]( + 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] = - provideFE[FAILURE2](f.pure[F]) + provideParallelFE[FAILURE2](f.pure[F]) // provideF - def provideF[FAILURE2 <: FAILURE: ClassTag]( + 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] = - provideFE(f.map(_.map(_.map { + provideParallelFE(f.map(_.map(_.map { case failure: FAILURE2 => Left(failure) case _: Unit => Right(()) }))) - inline def provideFE[FAILURE2 <: FAILURE]( + 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] = - provideAttemptFE(f.map(Right(_))) + provideParallelAttemptFE(f.map(Right(_))) // TODO Missing the union version - def provideAttemptFE[FAILURE2 <: FAILURE]( + 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 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 index 1c2d0a5..a0c76ff 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/logger/Logger.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/logger/Logger.scala @@ -22,6 +22,9 @@ object Logger: import cats.implicits.* + export NoopLogger.apply as noop + export ConsoleLogger.apply as console + sealed trait Level: def index: Int = this match diff --git a/core/src/test/scala/com/geirolz/app/toolkit/AppContextAndDependenciesSuite.scala b/core/src/test/scala/com/geirolz/app/toolkit/AppContextAndDependenciesSuite.scala index 7e71ed3..20d47ab 100644 --- a/core/src/test/scala/com/geirolz/app/toolkit/AppContextAndDependenciesSuite.scala +++ b/core/src/test/scala/com/geirolz/app/toolkit/AppContextAndDependenciesSuite.scala @@ -11,7 +11,7 @@ class AppContextAndDependenciesSuite extends munit.FunSuite: val res = App[IO] .withInfo(TestAppInfo.value) .withConsoleLogger() - .withConfig(TestConfig.defaultTest) + .withConfigPure(TestConfig.defaultTest) .withoutResources .withoutDependencies .provideOne(IO.unit) @@ -24,7 +24,7 @@ class AppContextAndDependenciesSuite extends munit.FunSuite: App[IO] .withInfo(TestAppInfo.value) .withConsoleLogger() - .withConfig(TestConfig.defaultTest) + .withConfigPure(TestConfig.defaultTest) .withoutDependencies .provideOne(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 77f1458..026ba48 100644 --- a/core/src/test/scala/com/geirolz/app/toolkit/AppSuite.scala +++ b/core/src/test/scala/com/geirolz/app/toolkit/AppSuite.scala @@ -25,8 +25,8 @@ class AppSuite extends munit.CatsEffectSuite { _ <- App[IO] .withInfo(TestAppInfo.value) .withConsoleLogger() - .withConfig(TestConfig.defaultTest) - .withResourcesResource(Resource.unit.trace(LabeledResource.appResources)) + .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() @@ -73,7 +73,7 @@ class AppSuite extends munit.CatsEffectSuite { _ <- App[IO] .withInfo(TestAppInfo.value) .withConsoleLogger() - .withConfig(TestConfig.defaultTest) + .withConfigPure(TestConfig.defaultTest) .dependsOn(Resource.pure[IO, Ref[IO, Int]](counter).trace(LabeledResource.appDependencies)) .provideOne(ctx.dependencies.set(1)) .compile() @@ -116,7 +116,7 @@ class AppSuite extends munit.CatsEffectSuite { _ <- App[IO] .withInfo(TestAppInfo.value) .withConsoleLogger() - .withConfig(TestConfig.defaultTest) + .withConfigPure(TestConfig.defaultTest) .dependsOn(Resource.unit[IO].trace(LabeledResource.appDependencies)) .provideOneF(IO.raiseError(error"BOOM!")) .compile() @@ -152,9 +152,9 @@ class AppSuite extends munit.CatsEffectSuite { _ <- App[IO] .withInfo(TestAppInfo.value) .withConsoleLogger() - .withConfig(TestConfig.defaultTest) + .withConfigPure(TestConfig.defaultTest) .withoutDependencies - .provide( + .provideParallel( List( IO.sleep(300.millis), IO.sleep(50.millis), @@ -194,7 +194,7 @@ class AppSuite extends munit.CatsEffectSuite { _ <- App[IO] .withInfo(TestAppInfo.value) .withConsoleLogger() - .withConfig(TestConfig.defaultTest) + .withConfigPure(TestConfig.defaultTest) .withoutDependencies .provideOne(IO.sleep(1.second)) .compile() @@ -230,9 +230,9 @@ class AppSuite extends munit.CatsEffectSuite { _ <- App[IO] .withInfo(TestAppInfo.value) .withConsoleLogger() - .withConfig(TestConfig.defaultTest) + .withConfigPure(TestConfig.defaultTest) .withoutDependencies - .provideF( + .provideParallelF( IO( List( IO.sleep(300.millis), @@ -276,7 +276,7 @@ class AppSuite extends munit.CatsEffectSuite { App[IO] .withInfo(TestAppInfo.value) .withConsoleLogger() - .withConfig(TestConfig.defaultTest) + .withConfigPure(TestConfig.defaultTest) .dependsOn(Resource.pure[IO, Unit](()).trace(LabeledResource.appDependencies)) .provideOne(IO.raiseError(error"BOOM!")) .compile() @@ -316,7 +316,7 @@ class AppSuite extends munit.CatsEffectSuite { _ <- App[IO] .withInfo(TestAppInfo.value) .withConsoleLogger() - .withConfig(TestConfig.defaultTest) + .withConfigPure(TestConfig.defaultTest) .withoutDependencies .beforeProviding( List( @@ -379,7 +379,7 @@ class AppSuite extends munit.CatsEffectSuite { _ <- App[IO] .withInfo(TestAppInfo.value) .withConsoleLogger() - .withConfig(TestConfig.defaultTest) + .withConfigPure(TestConfig.defaultTest) .withoutDependencies .provideOne( state.set( @@ -413,9 +413,9 @@ class AppSuite extends munit.CatsEffectSuite { app <- App[IO, AppError] .withInfo(TestAppInfo.value) .withConsoleLogger() - .withConfig(TestConfig.defaultTest) + .withConfigPure(TestConfig.defaultTest) .withoutDependencies - .provideE( + .provideParallelE( List( IO(Left(AppError.Boom())), IO.sleep(1.seconds) >> IO(Left(AppError.Boom())), @@ -451,9 +451,9 @@ class AppSuite extends munit.CatsEffectSuite { app <- App[IO, AppError] .withInfo(TestAppInfo.value) .withConsoleLogger() - .withConfig(TestConfig.defaultTest) + .withConfigPure(TestConfig.defaultTest) .withoutDependencies - .provideE { + .provideParallelE { List( IO(Left(AppError.Boom())), IO.sleep(1.seconds) >> IO(Left(AppError.Boom())), diff --git a/examples/buildinfo.properties b/examples/buildinfo.properties index ca931e9..8bf017b 100644 --- a/examples/buildinfo.properties +++ b/examples/buildinfo.properties @@ -1,2 +1,2 @@ -#Wed Jan 31 17:11:12 CET 2024 -buildnumber=240 +#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/com/geirolz/example/app/AppDependencyServices.scala b/examples/src/main/scala/com/geirolz/example/app/AppDependencyServices.scala index 615a6d7..bdc334d 100644 --- a/examples/src/main/scala/com/geirolz/example/app/AppDependencyServices.scala +++ b/examples/src/main/scala/com/geirolz/example/app/AppDependencyServices.scala @@ -1,20 +1,22 @@ package com.geirolz.example.app import cats.effect.{IO, Resource} -import com.geirolz.app.toolkit.{AppContext, ctx} -import com.geirolz.example.app.provided.KafkaConsumer +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] + kafkaConsumer: KafkaConsumer[IO], + hostTable: HostTable[IO] ) object AppDependencyServices: def resource(using - AppContext.NoDepsAndRes[AppInfo, SelfAwareStructuredLogger[IO], AppConfig] + AppContext.NoDeps[AppInfo, SelfAwareStructuredLogger[IO], AppConfig, AppResources] ): Resource[IO, AppDependencyServices] = Resource.pure( AppDependencyServices( - KafkaConsumer.fake(ctx.config.kafkaBroker.host) + kafkaConsumer = KafkaConsumer.fake(ctx.config.kafkaBroker.host), + hostTable = HostTable.fromString(ctx.resources.hostTableValues) ) ) diff --git a/examples/src/main/scala/com/geirolz/example/app/AppMain.scala b/examples/src/main/scala/com/geirolz/example/app/AppMain.scala index 45d5c45..b48a42c 100644 --- a/examples/src/main/scala/com/geirolz/example/app/AppMain.scala +++ b/examples/src/main/scala/com/geirolz/example/app/AppMain.scala @@ -11,14 +11,15 @@ import org.typelevel.log4cats.SelfAwareStructuredLogger import org.typelevel.log4cats.slf4j.Slf4jLogger object AppMain extends IOApp.Toolkit: - val app: App[IO, NoFailure, AppInfo, SelfAwareStructuredLogger, AppConfig, NoResources, AppDependencyServices] = + val app: App.Simple[IO, AppInfo, SelfAwareStructuredLogger, AppConfig, AppResources, AppDependencyServices] = App[IO] .withInfo(AppInfo.fromBuildInfo) - .withLogger(Slf4jLogger.getLogger[IO]) + .withLoggerPure(Slf4jLogger.getLogger[IO]) .withConfigF(pureconfigLoader[IO, AppConfig]) + .withResources(AppResources.resource) .dependsOnE(AppDependencyServices.resource.map(_.asRight)) .beforeProviding(ctx.logger.info("CUSTOM PRE-RUN")) - .provide( + .provideParallel( List( // HTTP server AppHttpServer.resource(ctx.config).useForever, 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 index c5ea103..ffde9c5 100644 --- a/examples/src/main/scala/com/geirolz/example/app/AppWithFailures.scala +++ b/examples/src/main/scala/com/geirolz/example/app/AppWithFailures.scala @@ -11,14 +11,15 @@ import org.typelevel.log4cats.slf4j.Slf4jLogger import cats.syntax.all.* object AppWithFailures extends IOApp.Toolkit: - val app: App[IO, AppError, AppInfo, SelfAwareStructuredLogger, AppConfig, NoResources, AppDependencyServices] = + val app: App[IO, AppError, AppInfo, SelfAwareStructuredLogger, AppConfig, AppResources, AppDependencyServices] = App[IO, AppError] .withInfo(AppInfo.fromBuildInfo) - .withLogger(Slf4jLogger.getLogger[IO]) + .withLoggerPure(Slf4jLogger.getLogger[IO]) .withConfigF(pureconfigLoader[IO, AppConfig]) + .withResources(AppResources.resource) .dependsOnE(AppDependencyServices.resource.map(_.asRight)) .beforeProviding(ctx.logger.info("CUSTOM PRE-RUN")) - .provide( + .provideParallel( List( // HTTP server AppHttpServer.resource(ctx.config).useForever, 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/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 fed25db..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,8 +2,7 @@ package com.geirolz.app.toolkit.fly4s import cats.effect.IO import com.geirolz.app.toolkit.fly4s.testing.TestConfig -import com.geirolz.app.toolkit.{App, AppMessages, SimpleAppInfo} -import com.geirolz.app.toolkit.App.ctx +import com.geirolz.app.toolkit.{ctx, App, AppMessages, SimpleAppInfo} class Fly4sSupportSuite extends munit.CatsEffectSuite: @@ -17,7 +16,7 @@ class Fly4sSupportSuite extends munit.CatsEffectSuite: sbtVersion = "1.8.0" ) ) - .withConfig( + .withConfigPure( TestConfig( dbUrl = "jdbc:postgresql://localhost:5432/toolkit", dbUser = Some("postgres"), 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 ef8fb79..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,7 +19,7 @@ class Log4CatsLoggerAdapterSuite extends munit.CatsEffectSuite { sbtVersion = "1.8.0" ) ) - .withLogger(NoOpLogger[IO]) + .withLoggerPure(NoOpLogger[IO]) .withoutDependencies .provideOne(IO.unit) .run() 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 4983842..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 @@ -18,7 +18,7 @@ class OdinLoggerAdapterSuite extends munit.CatsEffectSuite { sbtVersion = "1.8.0" ) ) - .withLogger(OdinLogger.noop[IO]) + .withLoggerPure(OdinLogger.noop[IO]) .withoutDependencies .provideOne(IO.unit) .run() From 62b599ac70c133ae4f3be09f888f4d99db658474 Mon Sep 17 00:00:00 2001 From: geirolz Date: Sat, 10 Feb 2024 15:16:21 +0100 Subject: [PATCH 20/22] Fmt --- .../scala/com/geirolz/app/toolkit/utils/ContextFunction.scala | 1 - 1 file changed, 1 deletion(-) 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 index f1ec62e..017042c 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/utils/ContextFunction.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/utils/ContextFunction.scala @@ -3,4 +3,3 @@ package com.geirolz.app.toolkit.utils extension [A, B](f: A => B) def asContextFunction: A ?=> B = f(summon[A]) - From 8e9babc3812d70d0f45ba9241abd924ac8ac5efb Mon Sep 17 00:00:00 2001 From: geirolz Date: Sat, 10 Feb 2024 15:52:01 +0100 Subject: [PATCH 21/22] Fix doc --- core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala | 5 ++--- docs/compiled/integrations.md | 2 +- docs/source/integrations.md | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala b/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala index 6a85170..46d1838 100644 --- a/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala +++ b/core/src/main/scala/com/geirolz/app/toolkit/AppBuilder.scala @@ -120,7 +120,6 @@ object AppBuilder: 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] @@ -131,7 +130,7 @@ object AppBuilder: 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 he end of the application. */ + /** 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] = @@ -140,7 +139,7 @@ object AppBuilder: case failure: FAILURE2 => Left(failure) }) - /** Dependencies are loaded into context and released at he end of the application. */ + /** 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] = diff --git a/docs/compiled/integrations.md b/docs/compiled/integrations.md index c290319..79050a0 100644 --- a/docs/compiled/integrations.md +++ b/docs/compiled/integrations.md @@ -172,7 +172,7 @@ App[IO] sbtVersion = "1.8.0" ) ) - .withPureConfig( + .withConfigPure( TestConfig( dbUrl = "jdbc:postgresql://localhost:5432/toolkit", dbUser = Some("postgres"), diff --git a/docs/source/integrations.md b/docs/source/integrations.md index 31be0cb..ad6663a 100644 --- a/docs/source/integrations.md +++ b/docs/source/integrations.md @@ -172,7 +172,7 @@ App[IO] sbtVersion = "1.8.0" ) ) - .withPureConfig( + .withConfigPure( TestConfig( dbUrl = "jdbc:postgresql://localhost:5432/toolkit", dbUser = Some("postgres"), From 33d01ebf09b3eb4f687ea80dd42d5f0f5c1b3320 Mon Sep 17 00:00:00 2001 From: geirolz Date: Thu, 15 Feb 2024 15:12:43 +0100 Subject: [PATCH 22/22] Update scala to 3.4.0 --- .github/workflows/cicd.yml | 4 ++-- .mergify.yml | 6 +++--- build.sbt | 4 ++-- docs/source/README.md | 8 ++++---- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 034279d..31f43b1 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -27,8 +27,8 @@ jobs: matrix: # supported scala versions include: - - scala: 3.3.1 - name: Scala3_3 + - scala: 3.4.0 + name: Scala3_4 test-tasks: scalafmtCheck gen-doc coverage test coverageReport steps: diff --git a/.mergify.yml b/.mergify.yml index ec5be09..eec5a17 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -4,7 +4,7 @@ pull_request_rules: - "#approved-reviews-by>=1" - check-success=codecov/patch - check-success=codecov/project - - check-success=build (Scala3_3) + - check-success=build (Scala3_4) - base=main - label!=work-in-progress actions: @@ -13,7 +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 (Scala3_3) + - check-success=build (Scala3_4) - base=main actions: merge: @@ -21,7 +21,7 @@ pull_request_rules: - name: automatic merge for master when CI passes and author is dependabot conditions: - author=dependabot[bot] - - check-success=build (Scala3_3) + - check-success=build (Scala3_4) - base=main actions: merge: diff --git a/build.sbt b/build.sbt index 3953666..025d460 100644 --- a/build.sbt +++ b/build.sbt @@ -3,8 +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 scala33 = "3.3.1" -lazy val supportedScalaVersions = List(scala33) +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.") diff --git a/docs/source/README.md b/docs/source/README.md index 6f06e1c..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