Skip to content

Commit

Permalink
With Implicit context
Browse files Browse the repository at this point in the history
  • Loading branch information
geirolz committed Jan 25, 2024
1 parent 2bda9b0 commit 2ba95ef
Show file tree
Hide file tree
Showing 36 changed files with 348 additions and 411 deletions.
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ lazy val baseSettings: Seq[Def.Setting[_]] = Seq(
def scalacSettings(scalaVersion: String): Seq[String] =
Seq(
"-encoding",
"-deprecation",
"utf-8", // Specify character encoding used by source files.
"-feature", // Emit warning and location for usages of features that should be imported explicitly.
"-language:existentials", // Existential types (besides wildcard types) can be written and inferred
Expand Down
17 changes: 17 additions & 0 deletions core/src/main/scala/com/geirolz/app/toolkit/=:!=.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.geirolz.app.toolkit

import scala.annotation.{implicitAmbiguous, implicitNotFound}
import scala.language.postfixOps

@implicitNotFound(msg = "Cannot prove that ${A} =:!= ${B}.")
sealed trait =:!=[A, B]
object =:!= {

given neq[A, B]: =:!=[A, B] = new =:!=[A, B] {}

@implicitAmbiguous(msg = "Expected a different type from ${A}")
given neqAmbig1[A]: =:!=[A, A] = null

@implicitAmbiguous(msg = "Expected a different type from ${A}")
given neqAmbig2[A]: =:!=[A, A] = null
}
184 changes: 91 additions & 93 deletions core/src/main/scala/com/geirolz/app/toolkit/App.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import cats.syntax.all.given
import cats.{Endo, Foldable, Parallel, Show}
import com.geirolz.app.toolkit.FailureHandler.OnFailureBehaviour
import com.geirolz.app.toolkit.logger.LoggerAdapter

import com.geirolz.app.toolkit.novalues.NoFailure
import com.geirolz.app.toolkit.novalues.NoFailure.NotNoFailure
import scala.reflect.ClassTag
import scala.util.NotGiven

class App[
F[+_]: Async: Parallel,
Expand All @@ -23,104 +23,47 @@ class App[
val loggerBuilder: F[LOGGER_T[F]],
val configLoader: Resource[F, CONFIG],
val resourcesLoader: Resource[F, RESOURCES],
val beforeProvidingF: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit],
val onFinalizeF: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit],
val failureHandlerLoader: AppResources[INFO, LOGGER_T[F], CONFIG, RESOURCES] => FailureHandler[F, FAILURE],
val depsLoader: AppResources[INFO, LOGGER_T[F], CONFIG, RESOURCES] => Resource[F, FAILURE \/ DEPENDENCIES],
val beforeProvidingTask: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit],
val onFinalizeTask: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit],
val failureHandlerLoader: AppContext[INFO, LOGGER_T[F], CONFIG, RESOURCES] ?=> FailureHandler[F, FAILURE],
val depsLoader: AppContext[INFO, LOGGER_T[F], CONFIG, RESOURCES] ?=> Resource[F, FAILURE \/ DEPENDENCIES],
val servicesBuilder: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[FAILURE \/ List[F[FAILURE \/ Unit]]]
):
type AppInfo = INFO
type Logger = LOGGER_T[F]
type Config = CONFIG
type Self = App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES]
private type SelfResources = AppResources[INFO, LOGGER_T[F], CONFIG, RESOURCES]

class Resourced[T](val res: SelfResources, val value: T):
export res.*
def map[U](f: T => U): Resourced[U] = Resourced(res, f(value))
def tupled: (SelfResources, T) = (res, value)
def useTupled[U](f: (SelfResources, T) => U): U = f.tupled(tupled)
def use[U](f: SelfResources => T => U): U = f(res)(value)
def tupledAll: (INFO, CONFIG, LOGGER_T[F], RESOURCES, T) = (info, config, logger, resources, value)
def useTupledAll[U](f: (INFO, CONFIG, LOGGER_T[F], RESOURCES, T) => U): U = f.tupled(tupledAll)

def onFinalize(
f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit]
): Self = copyWith(onFinalizeF = f)

def onFinalizeSeq(
type AppInfo = INFO
type Logger = LOGGER_T[F]
type Config = CONFIG
type Self = App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES]
type Context = AppContext[INFO, LOGGER_T[F], CONFIG, RESOURCES]

inline def onFinalizeSeq(
f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit],
fN: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => F[Unit]*
): Self =
onFinalizeSeq(deps => (f +: fN).map(_(deps)))

def onFinalizeSeq[G[_]: Foldable](
inline def onFinalizeSeq[G[_]: Foldable](
f: AppDependencies[INFO, LOGGER_T[F], CONFIG, DEPENDENCIES, RESOURCES] => G[F[Unit]]
): Self =
copyWith(onFinalizeF = f(_).sequence_)

// -------------------- AppFailureSyntax --------------------
// failures
def mapFailure[FAILURE2](
fhLoader: AppResources[INFO, LOGGER_T[F], CONFIG, RESOURCES] => FailureHandler[F, FAILURE2]
)(
f: FAILURE => FAILURE2
)(using NotNothing[FAILURE], NotNothing[FAILURE2]): App[F, FAILURE2, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
copyWith[F, FAILURE2, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES](
failureHandlerLoader = fhLoader,
dependenciesLoader = depsLoader.andThen(_.map(_.leftMap(f))),
provideBuilder = servicesBuilder.andThen(_.map(_.leftMap(f).map(_.map(_.map(_.leftMap(f))))))
)

def onFailure_(
f: Resourced[FAILURE] => F[Unit]
)(using NotNothing[FAILURE]): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
_updateFailureHandlerLoader(appRes => _.onFailure(failure => f(Resourced(appRes, failure)) >> failureHandlerLoader(appRes).onFailureF(failure)))

def onFailure(
f: Resourced[FAILURE] => F[OnFailureBehaviour]
)(using NotNothing[FAILURE]): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
_updateFailureHandlerLoader(appRes => _.onFailure(f.compose(Resourced(appRes, _))))

def handleFailureWith(
f: Resourced[FAILURE] => F[FAILURE \/ Unit]
)(using NotNothing[FAILURE]): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
_updateFailureHandlerLoader(appRes => _.handleFailureWith(f.compose(Resourced(appRes, _))))
copyWith(onFinalizeTask = f(_).sequence_)

// compile and run
def compile(appArgs: List[String])(using c: AppCompiler[F], i: AppLogicTypeInterpreter[F, FAILURE]): Resource[F, F[i.Result[Unit]]] =
inline def compile[R[_]](
appArgs: List[String] = Nil
)(using c: AppCompiler[F], i: AppLogicInterpreter[F, R, FAILURE]): Resource[F, F[R[Unit]]] =
i.interpret(c.compile(appArgs, this))

def run_(appArgs: List[String])(using AppCompiler[F], AppLogicTypeInterpreter[F, FAILURE]): F[Unit] =
run(appArgs).void

def run(appArgs: List[String])(using c: AppCompiler[F], i: AppLogicTypeInterpreter[F, FAILURE]): F[ExitCode] =
runRaw(appArgs)
inline def run[R[_]](appArgs: List[String] = Nil)(using c: AppCompiler[F], i: AppLogicInterpreter[F, R, FAILURE]): F[ExitCode] =
runRaw[R](appArgs)
.map(i.isSuccess(_))
.ifF(
ifTrue = ExitCode.Success,
ifFalse = ExitCode.Error
)

def runRaw(appArgs: List[String])(using c: AppCompiler[F], i: AppLogicTypeInterpreter[F, FAILURE]): F[i.Result[Unit]] =
inline def runRaw[R[_]](appArgs: List[String] = Nil)(using c: AppCompiler[F], i: AppLogicInterpreter[F, R, FAILURE]): F[R[Unit]] =
compile(appArgs).useEval

private def _updateFailureHandlerLoader(
fh: AppResources[INFO, LOGGER_T[F], CONFIG, RESOURCES] => Endo[FailureHandler[F, FAILURE]]
): App[
F,
FAILURE,
INFO,
LOGGER_T,
CONFIG,
RESOURCES,
DEPENDENCIES
] =
copyWith(
failureHandlerLoader = appRes => fh(appRes)(failureHandlerLoader(appRes))
)

private def copyWith[
private[toolkit] def copyWith[
G[+_]: Async: Parallel,
FAILURE2,
APP_INFO2 <: SimpleAppInfo[?],
Expand All @@ -129,15 +72,15 @@ class App[
RES2,
DEPS2
](
appInfo: APP_INFO2 = this.info,
appMessages: AppMessages = this.messages,
loggerBuilder: G[LOGGER_T2[G]] = this.loggerBuilder,
configLoader: Resource[G, CONFIG2] = this.configLoader,
resourcesLoader: Resource[G, RES2] = this.resourcesLoader,
beforeProvidingF: AppDependencies[APP_INFO2, LOGGER_T2[G], CONFIG2, DEPS2, RES2] => G[Unit] = this.beforeProvidingF,
onFinalizeF: AppDependencies[APP_INFO2, LOGGER_T2[G], CONFIG2, DEPS2, RES2] => G[Unit] = this.onFinalizeF,
failureHandlerLoader: AppResources[APP_INFO2, LOGGER_T2[G], CONFIG2, RES2] => FailureHandler[G, FAILURE2] = this.failureHandlerLoader,
dependenciesLoader: AppResources[APP_INFO2, LOGGER_T2[G], CONFIG2, RES2] => Resource[G, FAILURE2 \/ DEPS2] = this.depsLoader,
appInfo: APP_INFO2 = this.info,
appMessages: AppMessages = this.messages,
loggerBuilder: G[LOGGER_T2[G]] = this.loggerBuilder,
configLoader: Resource[G, CONFIG2] = this.configLoader,
resourcesLoader: Resource[G, RES2] = this.resourcesLoader,
beforeProvidingTask: AppDependencies[APP_INFO2, LOGGER_T2[G], CONFIG2, DEPS2, RES2] => G[Unit] = this.beforeProvidingTask,
onFinalizeTask: AppDependencies[APP_INFO2, LOGGER_T2[G], CONFIG2, DEPS2, RES2] => G[Unit] = this.onFinalizeTask,
failureHandlerLoader: AppContext[APP_INFO2, LOGGER_T2[G], CONFIG2, RES2] ?=> FailureHandler[G, FAILURE2] = this.failureHandlerLoader,
dependenciesLoader: AppContext[APP_INFO2, LOGGER_T2[G], CONFIG2, RES2] ?=> Resource[G, FAILURE2 \/ DEPS2] = this.depsLoader,
provideBuilder: AppDependencies[APP_INFO2, LOGGER_T2[G], CONFIG2, DEPS2, RES2] => G[FAILURE2 \/ List[G[FAILURE2 \/ Unit]]] = this.servicesBuilder
): App[G, FAILURE2, APP_INFO2, LOGGER_T2, CONFIG2, RES2, DEPS2] =
new App[G, FAILURE2, APP_INFO2, LOGGER_T2, CONFIG2, RES2, DEPS2](
Expand All @@ -146,17 +89,72 @@ class App[
loggerBuilder = loggerBuilder,
configLoader = configLoader,
resourcesLoader = resourcesLoader,
beforeProvidingF = beforeProvidingF,
onFinalizeF = onFinalizeF,
beforeProvidingTask = beforeProvidingTask,
onFinalizeTask = onFinalizeTask,
failureHandlerLoader = failureHandlerLoader,
depsLoader = dependenciesLoader,
servicesBuilder = provideBuilder
)

object App:
object App extends AppFailureSyntax:

inline def ctx[INFO <: SimpleAppInfo[?], LOGGER, CONFIG, RESOURCES](using
c: AppContext[INFO, LOGGER, CONFIG, RESOURCES]
): AppContext[INFO, LOGGER, CONFIG, RESOURCES] = c

def apply[F[+_]: Async: Parallel]: AppBuilder[F, Nothing] =
inline def apply[F[+_]: Async: Parallel]: AppBuilder[F, NoFailure] =
AppBuilder[F]

def apply[F[+_]: Async: Parallel, FAILURE: ClassTag]: AppBuilder[F, FAILURE] =
inline def apply[F[+_]: Async: Parallel, FAILURE: ClassTag]: AppBuilder[F, FAILURE] =
AppBuilder[F, FAILURE]

sealed trait AppFailureSyntax:

extension [
F[+_]: Async: Parallel,
FAILURE: NotNoFailure,
INFO <: SimpleAppInfo[?],
LOGGER_T[_[_]]: LoggerAdapter,
CONFIG: Show,
RESOURCES,
DEPENDENCIES
](app: App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES])

// failures
inline def mapFailure[FAILURE2](
fhLoader: AppContext[INFO, LOGGER_T[F], CONFIG, RESOURCES] ?=> FailureHandler[F, FAILURE2]
)(
f: FAILURE => FAILURE2
)(using NotNoFailure[FAILURE2]): App[F, FAILURE2, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
app.copyWith[F, FAILURE2, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES](
failureHandlerLoader = fhLoader,
dependenciesLoader = app.depsLoader.map(_.leftMap(f)),
provideBuilder = app.servicesBuilder.andThen(_.map(_.leftMap(f).map(_.map(_.map(_.leftMap(f))))))
)

inline def onFailure_(
f: app.Context ?=> FAILURE => F[Unit]
): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
_updateFailureHandlerLoader(_.onFailure(failure => f(failure) >> app.failureHandlerLoader.onFailureF(failure)))

inline def onFailure(
f: app.Context ?=> FAILURE => F[OnFailureBehaviour]
): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
_updateFailureHandlerLoader(_.onFailure(f))

inline def handleFailureWith(
f: app.Context ?=> FAILURE => F[FAILURE \/ Unit]
): App[F, FAILURE, INFO, LOGGER_T, CONFIG, RESOURCES, DEPENDENCIES] =
_updateFailureHandlerLoader(_.handleFailureWith(f))

private def _updateFailureHandlerLoader(
fh: AppContext[INFO, LOGGER_T[F], CONFIG, RESOURCES] ?=> Endo[FailureHandler[F, FAILURE]]
): App[
F,
FAILURE,
INFO,
LOGGER_T,
CONFIG,
RESOURCES,
DEPENDENCIES
] = app.copyWith(failureHandlerLoader = fh(app.failureHandlerLoader))
45 changes: 24 additions & 21 deletions core/src/main/scala/com/geirolz/app/toolkit/AppArgs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,40 +7,40 @@ import scala.util.Try

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

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

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

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

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

def at[V: ArgDecoder](idx: Int): Either[ArgDecodingError, V] =
inline def at[V: ArgDecoder](idx: Int): Either[ArgDecodingError, V] =
if (isDefinedAt(idx))
ArgDecoder[V].decode(value(idx))
else
Left(MissingArgAtIndex(idx))

def hasNotFlags(flag1: String, flagN: String*): Boolean =
inline def hasNotFlags(flag1: String, flagN: String*): Boolean =
!hasFlags(flag1, flagN*)

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

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

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

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

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

def getVar[V: ArgDecoder](name: String, separator: String = "="): Either[ArgDecodingError, V] = {
Expand All @@ -50,43 +50,46 @@ 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)] =
value.map(_.split(separator)).collect { case Array(key, value) =>
(key, value)
}

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

def isEmpty: Boolean = value.isEmpty
inline def isEmpty: Boolean =
value.isEmpty

def isDefinedAt(idx: Int): Boolean =
inline def isDefinedAt(idx: Int): Boolean =
value.isDefinedAt(idx)

override def toString: String = s"AppArgs(${value.mkString(", ")})"

private def orThrow[T](result: Either[ArgDecodingError, T]): T =
private inline def orThrow[T](result: Either[ArgDecodingError, T]): T =
result.fold(e => throw e.toException, identity)

object AppArgs:
def fromList(args: List[String]): AppArgs = AppArgs(args)
given Show[AppArgs] = Show.fromToString
inline def fromList(args: List[String]): AppArgs = AppArgs(args)
given Show[AppArgs] = Show.fromToString

// ---------------------------------
trait ArgDecoder[T]:
def decode(value: String): Either[ArgDecodingError, T]

object ArgDecoder:

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

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

sealed trait ArgDecodingError:
def toException = new RuntimeException(toString)
inline def toException = new RuntimeException(toString)
final override def toString: String = Show[ArgDecodingError].show(this)

object ArgDecodingError:
Expand Down
Loading

0 comments on commit 2ba95ef

Please sign in to comment.