Skip to content

Commit

Permalink
Merge pull request #40 from geirolz/Support_args
Browse files Browse the repository at this point in the history
Add args support
  • Loading branch information
geirolz authored Jun 8, 2023
2 parents 22ea1c6 + c8964dc commit 5db0313
Show file tree
Hide file tree
Showing 9 changed files with 295 additions and 100 deletions.
45 changes: 26 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# app-toolkit

[![Build Status](https://github.com/geirolz/app-toolkit/actions/workflows/cicd.yml/badge.svg)](https://github.com/geirolz/app-toolkit/actions)
[![codecov](https://img.shields.io/codecov/c/github/geirolz/app-toolkit)](https://codecov.io/gh/geirolz/app-toolkit)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/db3274b55e0c4031803afb45f58d4413)](https://www.codacy.com/manual/david.geirola/app-toolkit?utm_source=github.com&utm_medium=referral&utm_content=geirolz/app-toolkit&utm_campaign=Badge_Grade)
Expand All @@ -20,26 +21,28 @@ Check the full example [here](https://github.com/geirolz/app-toolkit/tree/main/e
- `provide` let you define the app provided services expressed by a `List[F[?]]` which will be run in parallel
- `provideF` let you define the app provided services expressed by a `F[List[F[?]]]` which will be run in parallel


Given

```scala
import cats.Show
import cats.effect.{ExitCode, Resource, IO, IOApp}
import com.geirolz.app.toolkit.{ App, SimpleAppInfo }
import com.geirolz.app.toolkit.{App, SimpleAppInfo}
import com.geirolz.app.toolkit.logger.ToolkitLogger
import com.geirolz.app.toolkit.novalues.NoResources
import org.typelevel.log4cats.slf4j.Slf4jLogger

// Define config
case class Config(host: String, port: Int)

object Config {
implicit val show: Show[Config] = Show.fromToString
}

// Define service dependencies
case class AppDependencyServices(
kafkaConsumer: KafkaConsumer[IO]
)
kafkaConsumer: KafkaConsumer[IO]
)

object AppDependencyServices {
def resource(res: App.Resources[SimpleAppInfo[String], ToolkitLogger[IO], Config, NoResources]): Resource[IO, AppDependencyServices] =
Resource.pure(AppDependencyServices(KafkaConsumer.fake))
Expand All @@ -49,10 +52,11 @@ object AppDependencyServices {
trait KafkaConsumer[F[_]] {
def consumeFrom(name: String): fs2.Stream[F, KafkaConsumer.KafkaRecord]
}

object KafkaConsumer {

import scala.concurrent.duration.DurationInt

case class KafkaRecord(value: String)

def fake: KafkaConsumer[IO] =
Expand All @@ -74,42 +78,45 @@ object Main extends IOApp {
App[IO]
.withInfo(
SimpleAppInfo.string(
name = "app-toolkit",
version = "0.0.1",
scalaVersion = "2.13.10",
sbtVersion = "1.8.0"
name = "app-toolkit",
version = "0.0.1",
scalaVersion = "2.13.10",
sbtVersion = "1.8.0"
)
)
)
.withLogger(ToolkitLogger.console[IO](_))
.withConfigLoader(_ => IO.pure(Config("localhost", 8080)))
.dependsOn(AppDependencyServices.resource(_))
.provideOne(deps =>
// Kafka consumer
deps.dependencies.kafkaConsumer
.consumeFrom("test-topic")
.evalTap(record => deps.logger.info(s"Received record $record"))
.compile
.drain
// Kafka consumer
deps.dependencies.kafkaConsumer
.consumeFrom("test-topic")
.evalTap(record => deps.logger.info(s"Received record $record"))
.compile
.drain
)
.beforeRun(_.logger.info("CUSTOM PRE-RUN"))
.onFinalize(_.logger.info("CUSTOM END"))
.run(ExitCode.Success)
.run(args)
}
```


### Integrations
#### pureconfig

#### pureconfig

```sbt
libraryDependencies += "com.github.geirolz" %% "app-toolkit-config-pureconfig" % "0.0.6"
```

#### log4cats

```sbt
libraryDependencies += "com.github.geirolz" %% "app-toolkit-log4cats" % "0.0.6"
```

#### odin

```sbt
libraryDependencies += "com.github.geirolz" %% "app-toolkit-odin" % "0.0.6"
```
88 changes: 59 additions & 29 deletions core/src/main/scala/com/geirolz/app/toolkit/App.scala
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class App[
): Self =
copyWith(onFinalizeF = f)

private[toolkit] def _compile: Resource[F, FAILURE \/ F[NonEmptyList[FAILURE] \/ Unit]] =
private[toolkit] def _compile(appArgs: List[String]): Resource[F, FAILURE \/ F[NonEmptyList[FAILURE] \/ Unit]] =
(
for {

Expand All @@ -94,6 +94,7 @@ class App[
// group resources
appResources: App.Resources[APP_INFO, LOGGER_T[F], CONFIG, RESOURCES] = App.Resources(
info = this.appInfo,
args = AppArgs(appArgs),
logger = appLogger,
config = appConfig,
resources = otherResources
Expand Down Expand Up @@ -216,17 +217,31 @@ object App extends AppSyntax {
import cats.syntax.all.*

final case class Dependencies[APP_INFO <: SimpleAppInfo[?], LOGGER, CONFIG, DEPENDENCIES, RESOURCES](
resources: App.Resources[APP_INFO, LOGGER, CONFIG, RESOURCES],
dependencies: DEPENDENCIES
private val _resources: App.Resources[APP_INFO, LOGGER, CONFIG, RESOURCES],
private val _dependencies: DEPENDENCIES
) {
// proxies
val info: APP_INFO = resources.info
val logger: LOGGER = resources.logger
val config: CONFIG = resources.config
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
}

final case class Resources[APP_INFO <: SimpleAppInfo[?], LOGGER, CONFIG, RESOURCES](
info: APP_INFO,
args: AppArgs,
logger: LOGGER,
config: CONFIG,
resources: RESOURCES
Expand All @@ -235,6 +250,15 @@ object App extends AppSyntax {
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
}

def apply[F[+_]: Async: Parallel](implicit dummyImplicit: DummyImplicit): AppBuilderRuntimeSelected[F, Throwable] =
Expand Down Expand Up @@ -467,22 +491,28 @@ sealed trait AppSyntax {
app._updateFailureHandlerLoader(appRes => _.handleFailureWith(f.compose(app.Resourced(appRes, _))))

// compile and run
def compile: Resource[F, FAILURE \/ F[NonEmptyList[FAILURE] \/ Unit]] =
app._compile

def run: F[ExitCode] = run {
case Left(_) => ExitCode.Error
case Right(_) => ExitCode.Success
}

def runReduce[B](f: FAILURE \/ Unit => B)(implicit semigroup: Semigroup[FAILURE]): F[B] =
run {
case Left(failures) => Left(failures.reduce)
case Right(_) => Right(())
}.map(f)

def run[B](f: NonEmptyList[FAILURE] \/ Unit => B): F[B] =
compile
def compile(args: List[String] = Nil): Resource[F, FAILURE \/ F[NonEmptyList[FAILURE] \/ Unit]] =
app._compile(args)

def run(appArgs: List[String] = Nil): F[ExitCode] =
runMap[ExitCode](appArgs).apply {
case Left(_) => ExitCode.Error
case Right(_) => ExitCode.Success
}

def runReduce[B](appArgs: List[String] = Nil, f: FAILURE \/ Unit => B)(implicit semigroup: Semigroup[FAILURE]): F[B] =
runMap[FAILURE \/ Unit](appArgs)
.apply {
case Left(failures) => Left(failures.reduce)
case Right(_) => Right(())
}
.map(f)

def runRaw(appArgs: List[String] = Nil): F[NonEmptyList[FAILURE] \/ Unit] =
runMap[NonEmptyList[FAILURE] \/ Unit](appArgs)(identity)

def runMap[B](appArgs: List[String] = Nil)(f: NonEmptyList[FAILURE] \/ Unit => B): F[B] =
compile(appArgs)
.map {
case Left(failure) => f(Left(NonEmptyList.one(failure))).pure[F]
case Right(appLogic) => appLogic.map(f)
Expand All @@ -497,8 +527,8 @@ sealed trait AppSyntax {
app: App[F, Throwable, APP_INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES]
) {

def compile: Resource[F, F[Unit]] =
app._compile.flatMap {
def compile(appArgs: List[String] = Nil): Resource[F, F[Unit]] =
app._compile(appArgs).flatMap {
case Left(failure) =>
Resource.raiseError(failure)
case Right(value) =>
Expand All @@ -509,13 +539,13 @@ sealed trait AppSyntax {
})
}

def run: F[ExitCode] =
run(ExitCode.Success)
def run_ : F[Unit] =
run().void

def run[U](b: U): F[U] =
compile
def run(appArgs: List[String] = Nil): F[ExitCode] =
compile(appArgs)
.use(_.pure[F])
.flatten
.as(b)
.as(ExitCode.Success)
}
}
117 changes: 117 additions & 0 deletions core/src/main/scala/com/geirolz/app/toolkit/AppArgs.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package com.geirolz.app.toolkit

import cats.Show
import com.geirolz.app.toolkit.ArgDecoder.{ArgDecodingError, MissingArgAtIndex, MissingVariable}

import scala.util.Try

final case class AppArgs(private val value: List[String]) extends AnyVal {

def exists(p: AppArgs => Boolean, pN: AppArgs => Boolean*): Boolean =
(p +: pN).forall(_.apply(this))

def stringAtOrThrow(idx: Int): String =
atOrThrow[String](idx)

def stringAt(idx: Int): Either[ArgDecodingError, String] =
at[String](idx)

def atOrThrow[V: ArgDecoder](idx: Int): V =
orThrow(at(idx))

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 =
!hasFlags(flag1, flagN*)

def hasFlags(flag1: String, flagN: String*): Boolean =
(flag1 +: flagN).forall(value.contains(_))

def hasNotVar(name: String, separator: String = "="): Boolean =
!hasVar(name, separator)

def hasVar(name: String, separator: String = "="): Boolean =
getStringVar(name, separator).isRight

def getStringVar(name: String, separator: String = "="): Either[ArgDecodingError, String] =
getVar[String](name, separator)

def getVarOrThrow[V: ArgDecoder](name: String, separator: String = "="): V =
orThrow(getVar(name, separator))

def getVar[V: ArgDecoder](name: String, separator: String = "="): Either[ArgDecodingError, V] = {
value.findLast(_.startsWith(s"$name$separator")).map(_.drop(name.length + separator.length)) match {
case Some(value) => ArgDecoder[V].decode(value)
case None => Left(MissingVariable(name))
}
}

def toMap(separator: String = "="): Map[String, String] =
toTuples(separator).toMap

def toTuples(separator: String = "="): List[(String, String)] =
value.map(_.split(separator)).collect { case Array(key, value) =>
(key, value)
}

def toList[V: ArgDecoder]: List[String] = value

def isEmpty: Boolean = value.isEmpty

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 =
result.fold(e => throw e.toException, identity)
}
object AppArgs {

def fromList(args: List[String]): AppArgs =
AppArgs(args)

implicit val show: Show[AppArgs] = Show.fromToString
}

trait ArgDecoder[T] {
def decode(value: String): Either[ArgDecodingError, T]
}
object ArgDecoder {

def apply[T: ArgDecoder]: ArgDecoder[T] = implicitly[ArgDecoder[T]]

def fromTry[T](t: String => Try[T]): ArgDecoder[T] =
(value: String) => t(value).toEither.left.map(ArgDecodingException(_))

sealed trait ArgDecodingError {
def toException = new RuntimeException(toString)
final override def toString: String = Show[ArgDecodingError].show(this)
}
object ArgDecodingError {
implicit val show: 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)))
}
Loading

0 comments on commit 5db0313

Please sign in to comment.