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