diff --git a/.scalafix.conf b/.scalafix.conf new file mode 100644 index 000000000..84a0d28fa --- /dev/null +++ b/.scalafix.conf @@ -0,0 +1,4 @@ +OrganizeImports { + groupedImports = Merge + removeUnused = true +} \ No newline at end of file diff --git a/build.sbt b/build.sbt index 20bb2762a..3e4e160a7 100644 --- a/build.sbt +++ b/build.sbt @@ -80,9 +80,10 @@ lazy val loggingStr = project lazy val loggingDer = project .in(file("logging/derivation")) .dependsOn(loggingStr) + .dependsOn(opticsMacro % "compile->test", derivation % "compile->test") .settings( defaultSettings, - libraryDependencies ++= Seq(derevo, magnolia), + libraryDependencies ++= Seq(derevo, magnolia, slf4j), publishName := "logging-derivation" ) @@ -316,18 +317,17 @@ lazy val defaultScalacOptions = scalacOptions := { } lazy val scalacWarningConfig = scalacOptions += { - // ignore unused imports that cannot be removed due to cross-compilation - val suppressUnusedImports = Seq[String]( - // put here relative file paths whose unused imports should be ignored, - // e.g. "scala/tofu/config/typesafe.scala" - ).map { src => - s"src=${scala.util.matching.Regex.quote(src)}&cat=unused-imports:iv" - } + // // ignore unused imports that cannot be removed due to cross-compilation + // val suppressUnusedImports = Seq( + // "scala/tofu/config/typesafe.scala" + // ).map { src => + // s"src=${scala.util.matching.Regex.quote(src)}&cat=unused-imports:s" + // }.mkString(",") // print warning category for fine-grained suppressing, e.g. @nowarn("cat=unused-params") val verboseWarnings = "any:wv" - s"-Wconf:${(suppressUnusedImports :+ verboseWarnings).mkString(",")}" + s"-Wconf:$verboseWarnings" } lazy val macros = Seq( diff --git a/config/src/main/scala/tofu/config/typesafe.scala b/config/src/main/scala/tofu/config/typesafe.scala index 1dc4d10f6..01fd3d87c 100644 --- a/config/src/main/scala/tofu/config/typesafe.scala +++ b/config/src/main/scala/tofu/config/typesafe.scala @@ -10,7 +10,6 @@ import tofu.concurrent.Refs import tofu.syntax.monadic._ import tofu.syntax.funk._ import cats.effect.SyncIO - import scala.annotation.nowarn object typesafe { diff --git a/core/src/main/scala/tofu/Delay.scala b/core/src/main/scala/tofu/Delay.scala new file mode 100644 index 000000000..03b161c63 --- /dev/null +++ b/core/src/main/scala/tofu/Delay.scala @@ -0,0 +1,19 @@ +package tofu + +import cats.effect.Sync + +trait Delay[F[_]] { + def delay[A](a: => A): F[A] +} + +object Delay extends CatsDelay { + type Safe[F[_, _]] = Delay[F[Nothing, *]] + type Catch[F[_, _]] = Delay[F[Throwable, *]] +} + +class CatsDelay { + implicit def byCatsSync[F[_]](implicit FS: Sync[F]): Delay[F] = + new Delay[F] { + def delay[A](a: => A): F[A] = FS.delay(a) + } +} diff --git a/core/src/main/scala/tofu/bi/BiContext.scala b/core/src/main/scala/tofu/bi/BiContext.scala index 21b8ebd5a..b66e9454c 100644 --- a/core/src/main/scala/tofu/bi/BiContext.scala +++ b/core/src/main/scala/tofu/bi/BiContext.scala @@ -1,19 +1,16 @@ package tofu.bi import cats.Bifunctor -import tofu.optics.Extract -import tofu.optics.Same -import tofu.optics.Contains import tofu.bi.lift.BiUnlift -import tofu.higherKind.bi.FunBK import tofu.control.Bind -import tofu.optics.Equivalent +import tofu.higherKind.bi.FunBK +import tofu.optics.{Contains, Equivalent, PExtract, Same} /** typeclass for access a functional environment in a bifuntor * @tparam X contextual error * @tparam C contextual result */ -trait BiContext[F[_, _], X, C] { +trait BiContext[F[+_, +_], +X, +C] { /** base F bifunctor inclusion */ @@ -29,7 +26,7 @@ trait BiContext[F[_, _], X, C] { * @param res context mapping in the optical form, this could be autogenerated `tofu.optics.Contains` * @return focused instance of context */ - def extract[E, A](err: Extract[X, E], res: Extract[C, A]): BiContext[F, E, A] = + def extract[E, A](err: PExtract[X, Any, E, Nothing], res: PExtract[C, Any, A, Nothing]): BiContext[F, E, A] = new BiContextExtractInstance[F, X, C, E, A](this, err, res) /** focus this context changing only the error @@ -37,23 +34,23 @@ trait BiContext[F[_, _], X, C] { * @param ex error mapping in the optical form * @return focused instance of context */ - def lextraxt[A](ex: Extract[C, A]): BiContext[F, X, A] = extract(Same.id, ex) + def lextraxt[A](ex: PExtract[C, Any, A, Nothing]): BiContext[F, X, A] = extract(Same.id, ex) /** focus this context changing only the result * * @param ex error mapping in the optical for * @return focused instance of context */ - def rextract[E](ex: Extract[X, E]): BiContext[F, E, C] = extract(ex, Same.id) + def rextract[E](ex: PExtract[X, Any, E, Nothing]): BiContext[F, E, C] = extract(ex, Same.id) } object BiContext { - def apply[F[_, _], X, C](implicit inst: BiContext[F, X, C]): BiContext[F, X, C] = inst + def apply[F[+_, +_], X, C](implicit inst: BiContext[F, X, C]): BiContext[F, X, C] = inst } /** typeclass for locally modification of environment for processess */ -trait BiLocal[F[_, _], X, C] extends BiContext[F, X, C] { +trait BiLocal[F[+_, +_], X, C] extends BiContext[F, X, C] { /** run the process in a locally modified environment * @@ -92,14 +89,14 @@ trait BiLocal[F[_, _], X, C] extends BiContext[F, X, C] { } object BiLocal { - def apply[F[_, _], X, C](implicit inst: BiLocal[F, X, C]): BiLocal[F, X, C] = inst + def apply[F[+_, +_], X, C](implicit inst: BiLocal[F, X, C]): BiLocal[F, X, C] = inst } /** typeclass relation for running processes with provided environment * @tparam F rich process type, that requires and has access the the environment * @tparam G base process type */ -trait BiRun[F[_, _], G[_, _], X, C] extends BiLocal[F, X, C] with BiUnlift[G, F] { +trait BiRun[F[+_, +_], G[+_, +_], X, C] extends BiLocal[F, X, C] with BiUnlift[G, F] { override def bifunctor: Bind[F] /** run a process starting from the error state for the environment @@ -144,35 +141,39 @@ trait BiRun[F[_, _], G[_, _], X, C] extends BiLocal[F, X, C] with BiUnlift[G, F] def runEitherK(ctx: Either[X, C]): F FunBK G = FunBK[F](runEither(_)(ctx)) override def bilocal[E, A](fea: F[E, A])(lproj: X => X, rproj: C => C): F[E, A] = - bifunctor.foldWith[X, C, E, A]( - context, + bifunctor.foldWith[X, C, E, A](context)( x => lift(runLeft(fea)(lproj(x))), c => lift(runRight(fea)(rproj(c))) ) override def disclose[E, A](k: FunBK[F, G] => F[E, A]): F[E, A] = - bifunctor.foldWith[X, C, E, A]( - context, + bifunctor.foldWith[X, C, E, A](context)( x => k(FunBK.apply(runLeft(_)(x))), c => k(FunBK.apply(runRight(_)(c))) ) } object BiRun { - def apply[F[_, _], G[_, _], X, C](implicit inst: BiRun[F, G, X, C]): BiRun[F, G, X, C] = inst + def apply[F[+_, +_], G[+_, +_], X, C](implicit inst: BiRun[F, G, X, C]): BiRun[F, G, X, C] = inst } -class BiContextExtractInstance[F[_, _], X, C, E, A](ctx: BiContext[F, X, C], lext: Extract[X, E], rext: Extract[C, A]) - extends BiContext[F, E, A] { +class BiContextExtractInstance[F[+_, +_], X, C, E, A]( + ctx: BiContext[F, X, C], + lext: PExtract[X, Any, E, Nothing], + rext: PExtract[C, Any, A, Nothing] +) extends BiContext[F, E, A] { override def bifunctor: Bifunctor[F] = ctx.bifunctor override def context: F[E, A] = bifunctor.bimap(ctx.context)(lext.extract, rext.extract) - override def extract[E1, A1](err: tofu.optics.Extract[E, E1], res: tofu.optics.Extract[A, A1]): BiContext[F, E1, A1] = - ctx.extract(lext >> err, rext >> res) + override def extract[E1, A1]( + err: PExtract[E, Any, E1, Nothing], + res: PExtract[A, Any, A1, Nothing] + ): BiContext[F, E1, A1] = + ctx.extract(lext >> err.as[Any, Nothing], rext >> res.as[Any, Nothing]) } -class BiLocalSubInstance[F[_, _], X, C, E, A](ctx: BiLocal[F, X, C], lcts: Contains[X, E], rcts: Contains[C, A]) +class BiLocalSubInstance[F[+_, +_], X, C, E, A](ctx: BiLocal[F, X, C], lcts: Contains[X, E], rcts: Contains[C, A]) extends BiContextExtractInstance[F, X, C, E, A](ctx, lcts, rcts) with BiLocal[F, E, A] { override def bilocal[E1, A1](fea: F[E1, A1])(lproj: E => E, rproj: A => A): F[E1, A1] = ctx.bilocal(fea)(lcts.update(_, lproj), rcts.update(_, rproj)) @@ -181,7 +182,7 @@ class BiLocalSubInstance[F[_, _], X, C, E, A](ctx: BiLocal[F, X, C], lcts: Conta ctx.sub(lcts >> err, rcts >> res) } -class BiRunEqvInstance[F[_, _], G[_, _], X, C, X1, C1]( +class BiRunEqvInstance[F[+_, +_], G[+_, +_], X, C, X1, C1]( ctx: BiRun[F, G, X, C], leq: Equivalent[X, X1], req: Equivalent[C, C1] diff --git a/core/src/main/scala/tofu/bi/package.scala b/core/src/main/scala/tofu/bi/package.scala index 5af5832f2..41f4d4c3e 100644 --- a/core/src/main/scala/tofu/bi/package.scala +++ b/core/src/main/scala/tofu/bi/package.scala @@ -1,10 +1,13 @@ package tofu package object bi { - type UContains[F[_, _], C] = BiContext[F, Nothing, C] - type ULocal[F[_, _], C] = BiLocal[F, Nothing, C] - type URun[F[_, _], G[_, _], C] = BiRun[F, G, Nothing, C] + type UContains[F[+_, +_], +C] = BiContext[F, Nothing, C] + type ULocal[F[+_, +_], C] = BiLocal[F, Nothing, C] + type URun[F[+_, +_], G[+_, +_], C] = BiRun[F, G, Nothing, C] type BiConst[A, B, C] = C + type TwinContext[F[+_, +_], +C] = BiContext[F, C, C] + type TwinLocal[F[+_, +_], C] = BiLocal[F, C, C] + } diff --git a/data/src/main/scala/tofu/data/Embedded.scala b/data/src/main/scala/tofu/data/Embedded.scala index 0dd4e3657..6190b2dc6 100644 --- a/data/src/main/scala/tofu/data/Embedded.scala +++ b/data/src/main/scala/tofu/data/Embedded.scala @@ -84,8 +84,7 @@ trait ExceptTInstances1 { def raise[E, A](e: E): ExceptT[F, E, A] = Embedded(e.asLeftF[F, A]) - def foldWith[E, A, X, R]( - fa: ExceptT[F, E, A], + def foldWith[E, A, X, R](fa: ExceptT[F, E, A])( h: E => ExceptT[F, X, R], f: A => ExceptT[F, X, R] ): ExceptT[F, X, R] = Embedded(fa.value.flatMap { diff --git a/data/src/main/scala/tofu/data/calc/CalcMInstances.scala b/data/src/main/scala/tofu/data/calc/CalcMInstances.scala index 6f94f1fef..fb5a75474 100644 --- a/data/src/main/scala/tofu/data/calc/CalcMInstances.scala +++ b/data/src/main/scala/tofu/data/calc/CalcMInstances.scala @@ -50,8 +50,7 @@ class CalcBindInstance[F[+_, +_], R, S] extends StackSafeBind[CalcM[F, R, S, S, override def raise[E, A](e: E): CalcM[F, R, S, S, E, A] = CalcM.raise(e) - override def foldWith[E, A, X, B]( - fa: CalcM[F, R, S, S, E, A], + override def foldWith[E, A, X, B](fa: CalcM[F, R, S, S, E, A])( h: E => CalcM[F, R, S, S, X, B], f: A => CalcM[F, R, S, S, X, B] ): CalcM[F, R, S, S, X, B] = fa.foldWith(f, h) @@ -68,7 +67,7 @@ class CalcContextInstance[F[+_, +_], R, S, E] extends WithRun[CalcM[F, R, S, S, } class CalcBiContextInstance[F[+_, +_], R, S] - extends BiRun[CalcM[F, R, S, S, *, *], CalcM[F, Any, S, S, *, *], Nothing, R] { + extends BiRun[CalcM[F, R, S, S, +*, +*], CalcM[F, Any, S, S, +*, +*], Nothing, R] { override def bifunctor: Bind[CalcM[F, R, S, S, *, *]] = CalcM.calcBindInstance override def lift[E, A](fa: CalcM[F, Any, S, S, E, A]): CalcM[F, R, S, S, E, A] = fa diff --git a/derivation/src/main/scala/tofu/data/derived/ContextEmbed.scala b/derivation/src/main/scala/tofu/data/derived/ContextEmbed.scala deleted file mode 100644 index ee25680e1..000000000 --- a/derivation/src/main/scala/tofu/data/derived/ContextEmbed.scala +++ /dev/null @@ -1,20 +0,0 @@ -package tofu.data.derived -import cats.FlatMap -import tofu.HasContext -import tofu.higherKind.Embed - -/** simple mixin for typeclass companion - * to add contextual embedded instance - */ -trait ContextEmbed[U[f[_]]] { - final implicit def contextEmbed[F[_]: FlatMap](implicit FH: F HasContext U[F], UE: Embed[U]): U[F] = - UE.embed(FH.context) -} - -/** mixin for something related to your monad - * for example context datatype companion - */ -trait EmbeddedInstances[F[_]] { - final implicit def contextEmbed[U[_[_]]](implicit FH: F HasContext U[F], UE: Embed[U], F: FlatMap[F]): U[F] = - UE.embed(FH.context) -} diff --git a/derivation/src/main/scala/tofu/data/derived/package.scala b/derivation/src/main/scala/tofu/data/derived/package.scala index ab60f94ee..9cf0a8714 100644 --- a/derivation/src/main/scala/tofu/data/derived/package.scala +++ b/derivation/src/main/scala/tofu/data/derived/package.scala @@ -1,5 +1,6 @@ package tofu.data package object derived { - type Merged[A] = Merged.Mer[A] + type Merged[A] = Merged.Mer[A] + type ContextEmbed[U[f[_]]] = tofu.higherKind.derived.ContextEmbed[U] } diff --git a/derivation/src/main/scala/tofu/higherKind/derived/ContextEmbed.scala b/derivation/src/main/scala/tofu/higherKind/derived/ContextEmbed.scala new file mode 100644 index 000000000..5f3b730dc --- /dev/null +++ b/derivation/src/main/scala/tofu/higherKind/derived/ContextEmbed.scala @@ -0,0 +1,39 @@ +package tofu.higherKind.derived +import cats.FlatMap +import tofu.HasContext +import tofu.bi.TwinContext +import tofu.control.Bind +import tofu.higherKind.Embed +import tofu.higherKind.bi.EmbedBK + +/** simple mixin for typeclass companion + * to add contextual embedded instance + */ +trait ContextEmbed[U[f[_]]] { + final implicit def contextEmbed[F[_]: FlatMap](implicit FH: F HasContext U[F], UE: Embed[U]): U[F] = + UE.embed(FH.context) +} + +/** mixin for something related to your monad + * for example context datatype companion + */ +trait EmbeddedInstances[F[_]] { + final implicit def contextEmbed[U[_[_]]](implicit FH: F HasContext U[F], UE: Embed[U], F: FlatMap[F]): U[F] = + UE.embed(FH.context) +} + +/** simple mixin for typeclass companion + * to add contextual embedded instance + */ +trait ContextBiEmbed[U[f[_, _]]] { + final implicit def contextEmbed[F[+_, +_]: Bind](implicit FH: F TwinContext U[F], UE: EmbedBK[U]): U[F] = + UE.biembed[F](FH.context) +} + +/** mixin for something related to your monad + * for example context datatype companion + */ +trait BiEmbeddedInstances[F[+_, +_]] { + final implicit def contextEmbed[U[f[_, _]]](implicit FH: F TwinContext U[F], UE: EmbedBK[U], F: Bind[F]): U[F] = + UE.biembed[F](FH.context) +} diff --git a/env/src/main/scala/tofu/env/bio/EnvBioInstances.scala b/env/src/main/scala/tofu/env/bio/EnvBioInstances.scala index 7a5c425e5..72bd54c79 100644 --- a/env/src/main/scala/tofu/env/bio/EnvBioInstances.scala +++ b/env/src/main/scala/tofu/env/bio/EnvBioInstances.scala @@ -8,7 +8,7 @@ import tofu.higherKind.bi.FunBK trait EnvBioInstances {} class EnvBioBifunctorInstance[R] - extends StackSafeBind[EnvBio[R, *, *]] with BiRun[EnvBio[R, *, *], BiTask, Nothing, R] { + extends StackSafeBind[EnvBio[R, *, *]] with BiRun[EnvBio[R, +*, +*], BiTask, Nothing, R] { override def disclose[E, A](k: FunBK[EnvBio[R, *, *], BiTask] => EnvBio[R, E, A]): EnvBio[R, E, A] = EnvBio.context.flatMap((ctx: R) => k(FunBK.apply(bio => bio.run(ctx)))) @@ -30,8 +30,7 @@ class EnvBioBifunctorInstance[R] override def raise[E, A](e: E): EnvBio[R, E, A] = EnvBio.raiseError(e) - override def foldWith[E, A, X, B]( - fa: EnvBio[R, E, A], + override def foldWith[E, A, X, B](fa: EnvBio[R, E, A])( h: E => EnvBio[R, X, B], f: A => EnvBio[R, X, B] ): EnvBio[R, X, B] = fa.foldWith(h, f) diff --git a/env/src/main/scala/tofu/env/bio/package.scala b/env/src/main/scala/tofu/env/bio/package.scala index 7d5302775..edc4957b4 100644 --- a/env/src/main/scala/tofu/env/bio/package.scala +++ b/env/src/main/scala/tofu/env/bio/package.scala @@ -3,5 +3,5 @@ package tofu.env import monix.eval.Task package object bio { - type BiTask[E, A] = Task[Either[E, A]] + type BiTask[+E, +A] = Task[Either[E, A]] } diff --git a/higherKindCore/src/main/scala/tofu/control/Bind.scala b/higherKindCore/src/main/scala/tofu/control/Bind.scala index 8be9a3356..349ce8bfc 100644 --- a/higherKindCore/src/main/scala/tofu/control/Bind.scala +++ b/higherKindCore/src/main/scala/tofu/control/Bind.scala @@ -27,10 +27,8 @@ trait TwinMonad[F[_, _]] extends BiMonad[F, F] with Bifunctor[F] { self => def raise[E, A](e: E): F[E, A] - def foldWith[E, A, X, R](fa: F[E, A], h: E => F[X, R], f: A => F[X, R]): F[X, R] - - final def foldWithC[E, A, X, R](fa: F[E, A])(h: E => F[X, R])(f: A => F[X, R]): F[X, R] = - foldWith(fa, h, f) + def foldWith[E, A, X, R](fa: F[E, A])(h: E => F[X, R], f: A => F[X, R]): F[X, R] + def foldWithC[E, A, X, R](fa: F[E, A])(h: E => F[X, R])(f: A => F[X, R]): F[X, R] = foldWith(fa)(h, f) def fromEither[E, A](ea: Either[E, A]): F[E, A] = ea match { case Left(e) => raise(e) @@ -38,19 +36,37 @@ trait TwinMonad[F[_, _]] extends BiMonad[F, F] with Bifunctor[F] { self => } def fold[E, A, X, R](fa: F[E, A])(h: E => R, f: A => R): F[X, R] = - foldWith[E, A, X, R](fa, e => pure(h(e)), a => pure(f(a))) + foldWith[E, A, X, R](fa)(e => pure(h(e)), a => pure(f(a))) def flatMap[E, A, B](fa: F[E, A], f: A => F[E, B]): F[E, B] = - foldWith(fa, raise[E, B], f) + foldWith(fa)(raise[E, B], f) + + def tapBoth[E, A, X, B, Y, C](fa: F[E, A])(h: E => F[X, B], f: A => F[Y, C]): F[E, A] = + foldWith(fa)(e => replaceErr(h(e))(e), a => replace(f(a))(a)) + + def flatTap[E, A, B](fa: F[E, A], f: A => F[E, B]): F[E, A] = + flatMap(fa, (a: A) => as(f(a))(a)) + + def tryTap[E, X, A, B](fa: F[E, A], f: A => F[X, B]): F[E, A] = + flatMap(fa, (a: A) => replace(f(a))(a)) def map[E, A, B](fa: F[E, A])(f: A => B): F[E, B] = flatMap(fa, (a: A) => pure(f(a))) def as[E, A, B](fa: F[E, A])(b: => B): F[E, B] = map(fa)(_ => b) + def replaceBoth[E, A, X, B](fa: F[E, A])(x: => X)(b: => B): F[X, B] = + foldWith(fa)(_ => raise(x), _ => pure(b)) + + def replace[E, A, X, B](fa: F[E, A])(b: => B): F[X, B] = + foldWith(fa)((_: E) => pure(b), (_: A) => pure(b)) + + def replaceErr[E, A, X, B](fa: F[E, A])(x: => X): F[X, B] = + foldWith(fa)(_ => raise(x), _ => raise(x)) + def void[E, A](fa: F[E, A]): F[E, Unit] = map(fa)(_ => ()) def flatMapErr[E, A, X](fa: F[E, A], f: E => F[X, A]): F[X, A] = - foldWith(fa, f, pure[X, A]) + foldWith(fa)(f, pure[X, A]) def mapErr[E, A, X](fa: F[E, A])(f: E => X): F[X, A] = flatMapErr(fa, (e: E) => raise(f(e))) @@ -59,7 +75,13 @@ trait TwinMonad[F[_, _]] extends BiMonad[F, F] with Bifunctor[F] { self => flatMap(fa, (a: A) => raise(f(a))) def handleWith[E, X, A](fa: F[E, A], h: E => F[X, A]): F[X, A] = - foldWith(fa, h, pure[X, A]) + foldWith(fa)(h, pure[X, A]) + + def handleTap[E, X, A](fa: F[E, A], f: E => F[X, A]): F[E, A] = + handleWith(fa, (e: E) => errAs(f(e))(e)) + + def tapError[E, X, A, B](fa: F[E, A], f: E => F[X, B]): F[E, A] = + handleWith(fa, (e: E) => replaceErr(f(e))(e)) def handle[E, X, A](fa: F[E, A], h: E => A): F[X, A] = handleWith[E, X, A](fa, e => pure(h(e))) @@ -69,20 +91,20 @@ trait TwinMonad[F[_, _]] extends BiMonad[F, F] with Bifunctor[F] { self => def voidErr[E, A](fa: F[E, A]): F[Unit, A] = mapErr(fa)(_ => ()) def bimap[A, B, C, D](fab: F[A, B])(f: A => C, g: B => D): F[C, D] = - foldWith[A, B, C, D](fab, a => raise(f(a)), b => pure(g(b))) + foldWith[A, B, C, D](fab)(a => raise(f(a)), b => pure(g(b))) def swapMap[E, A, X, B](fab: F[E, A])(f: E => B, g: A => X): F[X, B] = - foldWith[E, A, X, B](fab, e => pure(f(e)), a => raise(g(a))) + foldWith[E, A, X, B](fab)(e => pure(f(e)), a => raise(g(a))) - def swap[E, A](fab: F[E, A]): F[A, E] = foldWith[E, A, A, E](fab, e => pure(e), a => raise(a)) + def swap[E, A](fab: F[E, A]): F[A, E] = foldWith[E, A, A, E](fab)(e => pure(e), a => raise(a)) def left[A, B](a: A): F[A, B] = raise(a) def right[A, B](b: B): F[A, B] = pure(b) - def leftFlatMap[A, B, C, D](lab: F[A, B])(fl: A => F[C, D], fr: B => F[C, D]): F[C, D] = foldWith(lab, fl, fr) + def leftFlatMap[A, B, C, D](lab: F[A, B])(fl: A => F[C, D], fr: B => F[C, D]): F[C, D] = foldWith(lab)(fl, fr) - def rightFlatMap[A, B, C, D](rab: F[A, B])(fl: A => F[C, D], fr: B => F[C, D]): F[C, D] = foldWith(rab, fl, fr) + def rightFlatMap[A, B, C, D](rab: F[A, B])(fl: A => F[C, D], fr: B => F[C, D]): F[C, D] = foldWith(rab)(fl, fr) def bifunctor: Bifunctor[F] = new Bifunctor[F] { def bimap[A, B, C, D](fab: F[A, B])(f: A => C, g: B => D): F[C, D] = self.bimap(fab)(f, g) @@ -135,8 +157,7 @@ object Bind extends BindInstanceChain[Bind] { trait StackSafeBind[F[_, _]] extends Bind[F] { self => override def foldRec[E, A, X, B](init: Either[E, A])(step: Either[E, A] => F[Either[E, X], Either[A, B]]): F[X, B] = - foldWith[Either[E, X], Either[A, B], X, B]( - step(init), + foldWith[Either[E, X], Either[A, B], X, B](step(init))( { case Left(e) => foldRec(Left(e))(step) case Right(x) => raise(x) diff --git a/higherKindCore/src/main/scala/tofu/control/impl/BindInstanceChain.scala b/higherKindCore/src/main/scala/tofu/control/impl/BindInstanceChain.scala index 434ebc1dc..fc5a34eb5 100644 --- a/higherKindCore/src/main/scala/tofu/control/impl/BindInstanceChain.scala +++ b/higherKindCore/src/main/scala/tofu/control/impl/BindInstanceChain.scala @@ -16,7 +16,7 @@ trait BindInstanceChain[TC[f[_, _]] >: Bind[f]] { def raise[E, A](e: E): Either[E, A] = Left(e) - def foldWith[E, A, X, R](fa: Either[E, A], h: E => Either[X, R], f: A => Either[X, R]): Either[X, R] = fa.fold(h, f) + def foldWith[E, A, X, R](fa: Either[E, A])(h: E => Either[X, R], f: A => Either[X, R]): Either[X, R] = fa.fold(h, f) def foldRec[E, A, X, B](init: Either[E, A])( step: Either[E, A] => Either[Either[E, X], Either[A, B]] @@ -59,8 +59,7 @@ trait BindInstanceChain[TC[f[_, _]] >: Bind[f]] { def raise[E, A](e: E): EitherT[F, E, A] = EitherT.leftT(e) - def foldWith[E, A, X, R]( - fa: EitherT[F, E, A], + def foldWith[E, A, X, R](fa: EitherT[F, E, A])( h: E => EitherT[F, X, R], f: A => EitherT[F, X, R] ): EitherT[F, X, R] = diff --git a/higherKindCore/src/main/scala/tofu/higherKind/FunctionHK.scala b/higherKindCore/src/main/scala/tofu/higherKind/FunctionHK.scala new file mode 100644 index 000000000..d5040f16a --- /dev/null +++ b/higherKindCore/src/main/scala/tofu/higherKind/FunctionHK.scala @@ -0,0 +1,22 @@ +package tofu.higherKind + +/** Even more higher kinded function, + * a transformation between two higher order type-constructors + */ +trait FunctionHK[-U[F[_]], +V[F[_]]] { + def apply[F[_]](uf: U[F]): V[F] +} + +object FunctionHK { + def make[U[f[_]], V[f[_]]] = new Applied[U, V] + + class Applied[U[f[_]], V[f[_]]](private val __ : Boolean = true) extends AnyVal { + type Farb[_] + def apply(maker: Maker[U, V, Farb]): FunctionHK[U, V] = maker + } + + abstract class Maker[U[f[_]], V[f[_]], Farb[_]] extends FunctionHK[U, V] { + def apply[F[_]](uf: U[F]): V[F] = applyArb(uf.asInstanceOf[U[Farb]]).asInstanceOf[V[F]] + def applyArb(uf: U[Farb]): V[Farb] + } +} diff --git a/higherKindCore/src/main/scala/tofu/higherKind/Mid.scala b/higherKindCore/src/main/scala/tofu/higherKind/Mid.scala index c628e2f8a..abf35e723 100644 --- a/higherKindCore/src/main/scala/tofu/higherKind/Mid.scala +++ b/higherKindCore/src/main/scala/tofu/higherKind/Mid.scala @@ -4,7 +4,6 @@ import cats.tagless.ApplyK import cats.{Monoid, MonoidK, Semigroup} import tofu.higherKind.Mid.MidCompose import tofu.syntax.funk.funK -import tofu.syntax.monoidalK._ trait Mid[F[_], A] { def apply(fa: F[A]): F[A] diff --git a/higherKindCore/src/main/scala/tofu/higherKind/Point.scala b/higherKindCore/src/main/scala/tofu/higherKind/Point.scala index 55c5ac3ab..a6944601f 100644 --- a/higherKindCore/src/main/scala/tofu/higherKind/Point.scala +++ b/higherKindCore/src/main/scala/tofu/higherKind/Point.scala @@ -8,6 +8,9 @@ import cats.{ContravariantMonoidal, MonoidK, ~>} */ trait Point[F[_]] { def point[A]: F[A] + + def pureK[U[_[_]]](implicit U: PureK[U]): U[F] = U.pureK(this) + def pure[U[_[_]]: PureK]: U[F] = pureK } object Point { diff --git a/higherKindCore/src/main/scala/tofu/higherKind/bi/BiMid.scala b/higherKindCore/src/main/scala/tofu/higherKind/bi/BiMid.scala new file mode 100644 index 000000000..28257d4a4 --- /dev/null +++ b/higherKindCore/src/main/scala/tofu/higherKind/bi/BiMid.scala @@ -0,0 +1,63 @@ +package tofu.higherKind.bi +import cats.{Monoid, Semigroup} +import tofu.higherKind.bi.BiMid.BiMidCompose + +trait BiMid[F[_, _], E, A] { + def apply(fa: F[E, A]): F[E, A] + @inline def attach(fa: F[E, A]): F[E, A] = apply(fa) + + def compose(that: BiMid[F, E, A]): BiMid[F, E, A] = that.andThen(this) + def andThen(that: BiMid[F, E, A]): BiMid[F, E, A] = BiMidCompose(Vector(this, that)) +} + +object BiMid extends BiMidInstances { + def point[F[_, _]]: BiPoint[BiMid[F, *, *]] = new BiPoint[BiMid[F, *, *]] { + def apply[E, A]: BiMid[F, E, A] = x => x + } + + /** when unification fails */ + def attach[U[f[_, _]]: SemigroupalBK, F[_, _]](up: U[BiMid[F, *, *]])(alg: U[F]): U[F] = up.attach(alg) + + implicit final class TofuMidAlgebraSyntax[F[_, _], U[f[_, _]]](private val self: U[BiMid[F, *, *]]) extends AnyVal { + def attach(alg: U[F])(implicit U: SemigroupalBK[U]): U[F] = + U.map2b(alg, self)(Fun2BK.apply((a, b) => b(a))) + } + + private final case class BiMidCompose[F[_, _], E, A](elems: Vector[BiMid[F, E, A]]) extends BiMid[F, E, A] { + override def apply(fa: F[E, A]): F[E, A] = elems.foldLeft(fa)((x, m) => m(x)) + override def compose(that: BiMid[F, E, A]): BiMid[F, E, A] = that match { + case BiMidCompose(es) => BiMidCompose(elems ++ es) + case _ => BiMidCompose(elems :+ that) + } + override def andThen(that: BiMid[F, E, A]): BiMid[F, E, A] = that match { + case BiMidCompose(es) => BiMidCompose(es ++ elems) + case _ => BiMidCompose(that +: elems) + } + } + +} +trait BiMidInstances extends BiMidInstances1 { + implicit def midMonoidBK[F[_, _]]: MonoidBK[BiMid[F, *, *]] = new BiMidMonoidK[F] + + implicit def midAlgebraMonoid[F[_, _], U[f[_, _]]: MonoidalBK]: Monoid[U[BiMid[F, *, *]]] = + new BiMidAlgebraMonoid[F, U] +} + +trait BiMidInstances1 { + implicit def midAlgebraSemigroup[F[_, _], U[f[_, _]]: SemigroupalBK]: Semigroup[U[BiMid[F, *, *]]] = + new BiMidAlgebraSemigroup[F, U] +} + +class BiMidMonoidK[F[_, _]] extends MonoidBK[BiMid[F, *, *]] { + def emptybk[E, A]: BiMid[F, E, A] = fa => fa + def combinebk[E, A](x: BiMid[F, E, A], y: BiMid[F, E, A]): BiMid[F, E, A] = fa => x(y(fa)) +} + +class BiMidAlgebraMonoid[F[_, _], U[f[_, _]]: MonoidalBK] + extends BiMidAlgebraSemigroup[F, U] with Monoid[U[BiMid[F, *, *]]] { + def empty: U[BiMid[F, *, *]] = BiMid.point[F].pure +} +class BiMidAlgebraSemigroup[F[_, _], U[f[_, _]]](implicit U: SemigroupalBK[U]) extends Semigroup[U[BiMid[F, *, *]]] { + def combine(x: U[BiMid[F, *, *]], y: U[BiMid[F, *, *]]): U[BiMid[F, *, *]] = + U.map2b(x, y)(Fun2BK.apply((m1, m2) => fa => m1(m2(fa)))) +} diff --git a/higherKindCore/src/main/scala/tofu/higherKind/bi/BiPoint.scala b/higherKindCore/src/main/scala/tofu/higherKind/bi/BiPoint.scala index 5cb0c30aa..e16487cc3 100644 --- a/higherKindCore/src/main/scala/tofu/higherKind/bi/BiPoint.scala +++ b/higherKindCore/src/main/scala/tofu/higherKind/bi/BiPoint.scala @@ -2,6 +2,8 @@ package tofu.higherKind.bi trait BiPoint[F[_, _]] { def apply[E, A]: F[E, A] + + def pure[U[f[_, _]]](implicit UP: PureBK[U]): U[F] = UP.pureB(this) } object BiPoint { diff --git a/higherKindCore/src/main/scala/tofu/higherKind/bi/MonoidBK.scala b/higherKindCore/src/main/scala/tofu/higherKind/bi/MonoidBK.scala new file mode 100644 index 000000000..2f9842310 --- /dev/null +++ b/higherKindCore/src/main/scala/tofu/higherKind/bi/MonoidBK.scala @@ -0,0 +1,25 @@ +package tofu.higherKind.bi + +import cats.MonoidK +import cats.kernel.Monoid +trait MonoidBK[F[_, _]] extends SemigroupBK[F] { + def emptybk[A, B]: F[A, B] + + def monoidK[X]: MonoidK[F[X, *]] = new MonoidK[F[X, *]] { + def combineK[A](x: F[X, A], y: F[X, A]): F[X, A] = combinebk(x, y) + + def empty[A]: F[X, A] = emptybk + } + + def leftMonoidK[X, Y]: MonoidK[F[*, X]] = new MonoidK[F[*, X]] { + def combineK[A](x: F[A, X], y: F[A, X]): F[A, X] = combinebk(x, y) + + def empty[A]: F[A, X] = emptybk + } + + def monoid[X, Y]: Monoid[F[X, Y]] = new Monoid[F[X, Y]] { + def combine(x: F[X, Y], y: F[X, Y]): F[X, Y] = combinebk(x, y) + + def empty: F[X, Y] = emptybk + } +} diff --git a/higherKindCore/src/main/scala/tofu/higherKind/bi/SemigroupBK.scala b/higherKindCore/src/main/scala/tofu/higherKind/bi/SemigroupBK.scala new file mode 100644 index 000000000..df1a83048 --- /dev/null +++ b/higherKindCore/src/main/scala/tofu/higherKind/bi/SemigroupBK.scala @@ -0,0 +1,18 @@ +package tofu.higherKind.bi + +import cats.SemigroupK +import cats.kernel.Semigroup + +trait SemigroupBK[F[_, _]] { + def combinebk[A, B](x: F[A, B], y: F[A, B]): F[A, B] + + def semigroupK[X]: SemigroupK[F[X, *]] = new SemigroupK[F[X, *]] { + def combineK[A](x: F[X, A], y: F[X, A]): F[X, A] = combinebk(x, y) + } + + def leftSemigroupK[X]: SemigroupK[F[*, X]] = new SemigroupK[F[*, X]] { + def combineK[A](x: F[A, X], y: F[A, X]): F[A, X] = combinebk(x, y) + } + + def semigroup[X, Y]: Semigroup[F[X, Y]] = combinebk(_, _) +} diff --git a/higherKindCore/src/main/scala/tofu/higherKind/bi/extensions.scala b/higherKindCore/src/main/scala/tofu/higherKind/bi/extensions.scala new file mode 100644 index 000000000..06f91fe0b --- /dev/null +++ b/higherKindCore/src/main/scala/tofu/higherKind/bi/extensions.scala @@ -0,0 +1,6 @@ +package tofu.higherKind.bi + +class MonoidBKOps[F[_, _], A, B](private val fab: F[A, B]) extends AnyVal { + def combinebk[F1[x, y] >: F[x, y]](other: F1[A, B])(implicit FSG: SemigroupBK[F1]): F1[A, B] = + FSG.combinebk(fab, other) +} diff --git a/higherKindCore/src/main/scala/tofu/higherKind/derived/HigherKindedMacros.scala b/higherKindCore/src/main/scala/tofu/higherKind/derived/HigherKindedMacros.scala index e0af8e1a1..b3b28c8d1 100644 --- a/higherKindCore/src/main/scala/tofu/higherKind/derived/HigherKindedMacros.scala +++ b/higherKindCore/src/main/scala/tofu/higherKind/derived/HigherKindedMacros.scala @@ -145,6 +145,65 @@ class HigherKindedMacros(override val c: blackbox.Context) extends cats.tagless. override def embMethod(retConst: Type): Tree = q"${summon[EmbedBK[Any]](retConst)}.biembed" }) + /** Implement a possibly refined `algebra` with the provided `members`. */ + def implementSimple(applied: Type)(members: Iterable[Tree]): Tree = { + // If `members.isEmpty` we need an extra statement to ensure the generation of an anonymous class. + val nonEmptyMembers = if (members.isEmpty) q"()" :: Nil else members + + applied match { + case RefinedType(parents, scope) => + val refinements = delegateTypes(applied, scope.filterNot(_.isAbstract)) { (tpe, _) => + tpe.typeSignatureIn(applied).resultType + } + + q"new ..$parents { ..$refinements; ..$nonEmptyMembers }" + case _ => + q"new $applied { ..$nonEmptyMembers }" + } + } + + type Place + val Place = symbolOf[Place] + + def factorizeApply( + builder: Tree, + Alg: Type, + F: Type, + ): Tree = { + val Af = Alg match { + case PolyType(_, TypeRef(t, s, as)) => typeRef(t, s, as.init :+ F) + case _ => appliedType(Alg, List(F)) + } + + val members = overridableMembersOf(Alg) + val types = delegateAbstractTypes(Alg, members, Alg) + + val prepared = c.freshName(TermName("builder")) + val methods = delegateMethods(Af, members, NoSymbol) { case method => + val methodName = method.name.encodedName.toString + val start: Tree = method.returnType match { + case TypeRef(_, _, xs) => q"$prepared.start[..$xs]($methodName)" + case _ => + c.abort(c.enclosingPosition, s"can't unpack ${method.name}'s return type: ${method.returnType}") + } + + val withParams = + method.paramLists.iterator.flatten + .filter(vd => !vd.mods.hasFlag(Flag.IMPLICIT)) + .foldLeft(start) { + case (b, ValDef(_, param, _, _)) => + q"$b.arg(${param.encodedName.toString()}, $param)" + case (_, p) => c.abort(c.enclosingPosition, s"unexpected error during handling argument $p of $method") + } + + method.copy(body = q"$withParams.result") + } + + q""" + val $prepared = $builder.prepare[$Alg] + ${implementSimple(Af)(types ++ methods)}""" + } + def representableK[Alg[_[_]]](implicit tag: WeakTypeTag[Alg[Any]]): Tree = instantiate[RepresentableK[Alg]](tag)(tabulate, productK, mapK, embedf) @@ -156,4 +215,30 @@ class HigherKindedMacros(override val c: blackbox.Context) extends cats.tagless. def embedB[Alg[_[_, _]]](implicit tag: WeakTypeTag[Alg[Any]]): Tree = instantiate[EmbedBK[Alg]](tag)(biembed) + + def factorize[Builder, F[_], Alg[_[_]]](builder: Tree)(implicit + F: WeakTypeTag[F[Any]], + Alg: WeakTypeTag[Alg[Any]] + ): Tree = factorizeApply(builder, Alg.tpe, F.tpe) + + def factorizeThis[Alg[_[_]]](implicit + Alg: WeakTypeTag[Alg[Any]] + ): Tree = { + + val that = c.typecheck(tq"${c.prefix.tree}.Result", mode = c.TYPEmode).tpe + factorizeApply(c.prefix.tree, Alg.tpe, that) + } + + def bifactorize[Builder, F[_, _], Alg[_[_, _]]](builder: Tree)(implicit + F: WeakTypeTag[F[Any, Any]], + Alg: WeakTypeTag[Alg[Any]] + ): Tree = factorizeApply(builder, Alg.tpe, F.tpe) + + def bifactorizeThis[Alg[_[_, _]]](implicit + Alg: WeakTypeTag[Alg[Any]] + ): Tree = { + val that = c.typecheck(tq"${c.prefix.tree}.Result", mode = c.TYPEmode).tpe + factorizeApply(c.prefix.tree, Alg.tpe, that) + } + } diff --git a/higherKindCore/src/main/scala/tofu/higherKind/derived/derived.scala b/higherKindCore/src/main/scala/tofu/higherKind/derived/derived.scala index 76f8850a2..50678c04f 100644 --- a/higherKindCore/src/main/scala/tofu/higherKind/derived/derived.scala +++ b/higherKindCore/src/main/scala/tofu/higherKind/derived/derived.scala @@ -9,4 +9,10 @@ package object derived { def genRepresentableB[Alg[_[_, _]]]: RepresentableB[Alg] = macro HigherKindedMacros.representableB[Alg] def genEmbedB[Alg[_[_, _]]]: EmbedBK[Alg] = macro HigherKindedMacros.embedB[Alg] + + def factorize[Builder, F[_], Alg[_[_]]](builder: Builder): Alg[F] = + macro HigherKindedMacros.factorize[Builder, F, Alg] + + def bifactorize[Builder, F[_, _], Alg[_[_, _]]](builder: Builder): Alg[F] = + macro HigherKindedMacros.bifactorize[Builder, F, Alg] } diff --git a/higherKindCore/src/main/scala/tofu/syntax/bind.scala b/higherKindCore/src/main/scala/tofu/syntax/bind.scala index f81e8ad52..2206b2a4a 100644 --- a/higherKindCore/src/main/scala/tofu/syntax/bind.scala +++ b/higherKindCore/src/main/scala/tofu/syntax/bind.scala @@ -7,16 +7,29 @@ object bind { def flatMap[F1[e, a] >: F[e, a], E1 >: E, B](f: A => F1[E1, B])(implicit F: TwinMonad[F1]): F1[E1, B] = F.flatMap(self, f) + def flatTap[F1[e, a] >: F[e, a], E1 >: E, B](f: A => F1[E1, B])(implicit F: TwinMonad[F1]): F1[E1, A] = + F.flatTap(self, f) + def map[B](f: A => B)(implicit F: TwinMonad[F]): F[E, B] = F.map(self)(f) + def tapBoth[F1[e, a] >: F[e, a], X, B, Y, C](h: E => F1[X, B], f: A => F1[Y, C])(implicit + F: TwinMonad[F1] + ): F1[E, A] = F.tapBoth(self)(h, f) + + def *>[F1[e, a] >: F[e, a], E1 >: E, B](fb: F1[E1, B])(implicit F: TwinMonad[F1]): F1[E1, B] = flatMap(_ => fb) + def mapErr[X](f: E => X)(implicit F: TwinMonad[F]): F[X, A] = F.mapErr(self)(f) def handleWith[F1[e, a] >: F[e, a], X, A1 >: A](f: E => F1[X, A1])(implicit F: TwinMonad[F1]): F1[X, A1] = F.handleWith(self, f) + def handleTap[F1[e, a] >: F[e, a], X, A1 >: A](f: E => F1[X, A1])(implicit F: TwinMonad[F1]): F1[E, A1] = + F.handleTap(self, f) + def handle[A1 >: A](f: E => A1)(implicit F: TwinMonad[F]): F[Nothing, A1] = F.handle(self, f) - def foldWith[X, R](h: E => F[X, R], f: A => F[X, R])(implicit F: TwinMonad[F]): F[X, R] = F.foldWith(self, h, f) + def foldWith[F1[e, a] >: F[e, a], X, R](h: E => F1[X, R], f: A => F1[X, R])(implicit F: TwinMonad[F1]): F1[X, R] = + F.foldWith(self)(h, f) def fold[R](h: E => R, f: A => R)(implicit F: TwinMonad[F]): F[Nothing, R] = F.fold(self)(h, f) @@ -41,3 +54,55 @@ object bind { def swap(implicit F: TwinMonad[F]): F[A, E] = F.swap(self) } } + +object bindInv { + implicit class BindInvariantSyntax[F[+_, +_], E, A](private val self: F[E, A]) extends AnyVal { + def flatMap[E1 >: E, B](f: A => F[E1, B])(implicit F: TwinMonad[F]): F[E1, B] = + F.flatMap(self, f) + + def flatTap[E1 >: E, B](f: A => F[E1, B])(implicit F: TwinMonad[F]): F[E1, A] = + F.flatTap(self, f) + + def map[B](f: A => B)(implicit F: TwinMonad[F]): F[E, B] = F.map(self)(f) + + def tapBoth[X, B, Y, C](h: E => F[X, B], f: A => F[Y, C])(implicit + F: TwinMonad[F] + ): F[E, A] = F.tapBoth(self)(h, f) + + def *>[E1 >: E, B](fb: F[E1, B])(implicit F: TwinMonad[F]): F[E1, B] = flatMap(_ => fb) + + def mapErr[X](f: E => X)(implicit F: TwinMonad[F]): F[X, A] = F.mapErr(self)(f) + + def handleWith[X, A1 >: A](f: E => F[X, A1])(implicit F: TwinMonad[F]): F[X, A1] = + F.handleWith(self, f) + + def handleTap[X, A1 >: A](f: E => F[X, A1])(implicit F: TwinMonad[F]): F[E, A1] = + F.handleTap(self, f) + + def handle[A1 >: A](f: E => A1)(implicit F: TwinMonad[F]): F[Nothing, A1] = F.handle(self, f) + + def foldWith[X, R](h: E => F[X, R], f: A => F[X, R])(implicit F: TwinMonad[F]): F[X, R] = F.foldWith(self)(h, f) + + def fold[R](h: E => R, f: A => R)(implicit F: TwinMonad[F]): F[Nothing, R] = F.fold(self)(h, f) + + def as[B](b: B)(implicit F: TwinMonad[F]): F[E, B] = F.as(self)(b) + + def void(implicit F: TwinMonad[F]): F[E, Unit] = F.void(self) + + def flatMapErr[X](f: E => F[X, A])(implicit F: TwinMonad[F]): F[X, A] = + F.flatMapErr(self, f) + + def fail[B](f: A => E)(implicit F: TwinMonad[F]): F[E, B] = F.fail(self)(f) + + def errAs[X](x: => X)(implicit F: TwinMonad[F]) = F.errAs(self)(x) + + def voidErr(implicit F: TwinMonad[F]): F[Unit, A] = F.voidErr(self) + + def bimap[X, R](f: E => X, g: A => R)(implicit F: TwinMonad[F]): F[X, R] = F.bimap(self)(f, g) + + def swapMap[X, B](f: E => B, g: A => X)(implicit F: TwinMonad[F]): F[X, B] = + F.swapMap(self)(f, g) + + def swap(implicit F: TwinMonad[F]): F[A, E] = F.swap(self) + } +} diff --git a/higherKindCore/src/main/scala/tofu/syntax/functorbk.scala b/higherKindCore/src/main/scala/tofu/syntax/functorbk.scala new file mode 100644 index 000000000..9cbf854fe --- /dev/null +++ b/higherKindCore/src/main/scala/tofu/syntax/functorbk.scala @@ -0,0 +1,13 @@ +package tofu.syntax + +import tofu.higherKind.bi.FunBK +import tofu.higherKind.bi.FunctorBK +import tofu.higherKind.bi.Fun2BK +import tofu.higherKind.bi.SemigroupalBK + +object functorbk { + implicit class TofuFunctorBKOps[U[f[_, _]], F[_, _]](private val uf: U[F]) extends AnyVal { + def mapb[G[_, _]](f: F FunBK G)(implicit U: FunctorBK[U]): U[G] = U.mapb(uf)(f) + def map2b[G[_, _], H[_, _]](ug: U[G])(f: Fun2BK[F, G, H])(implicit U: SemigroupalBK[U]): U[H] = U.map2b(uf, ug)(f) + } +} diff --git a/higherKindCore/src/main/scala/tofu/syntax/monoidalK.scala b/higherKindCore/src/main/scala/tofu/syntax/monoidalK.scala index 189efd98a..02e4af471 100644 --- a/higherKindCore/src/main/scala/tofu/syntax/monoidalK.scala +++ b/higherKindCore/src/main/scala/tofu/syntax/monoidalK.scala @@ -2,6 +2,7 @@ package tofu.syntax import tofu.higherKind.{Function2K, MonoidalK, Point, PureK} object monoidalK { + @deprecated("this extension moved to the [Point] itself", since = "0.10.1") implicit final class TofuPointKOps[F[_]](private val point: Point[F]) extends AnyVal { def pureK[U[_[_]]](implicit U: PureK[U]): U[F] = U.pureK(point) } diff --git a/higherKindCore/src/test/scala/tofu/higherKind/FactorizeSuite.scala b/higherKindCore/src/test/scala/tofu/higherKind/FactorizeSuite.scala new file mode 100644 index 000000000..15077463f --- /dev/null +++ b/higherKindCore/src/test/scala/tofu/higherKind/FactorizeSuite.scala @@ -0,0 +1,128 @@ +package tofu.higherKind + +import scala.reflect.ClassTag +import scala.reflect.classTag + +import cats.Show +import cats.syntax.show._ +import org.scalatest.funsuite.AnyFunSuite +import tofu.higherKind.derived.HigherKindedMacros + +class FactorizeSuite extends AnyFunSuite { + import FactorizeSuite._ + + type MapF[A] = MyMap + + def testFactorization(fooMap: Foo[MapF]) = { + assert( + fooMap.building[Int](100)(List(1, 2, 3)) === + Map( + "" -> (classOf[Foo[Any]] -> ""), + "building" -> (classOf[List[Any]] -> ""), + "weight" -> (classOf[Double] -> (100: Double).show), + "elems" -> (classOf[List[Int]] -> List(1, 2, 3).show), + ) + ) + + assert( + fooMap.person("Oli", 26) === + Map( + "" -> (classOf[Foo[Any]] -> ""), + "person" -> (classOf[Unit] -> ""), + "name" -> (classOf[String] -> "Oli"), + "age" -> (classOf[Int] -> "26"), + ) + ) + } + + test("simple factorization")(testFactorization(derived.factorize[Builder.type, MapF, Foo](Builder))) + + import FactorizeSuite.{Builder => `strange and fancy name`} + import `strange and fancy name`.{conjure => `🤫`} + + test("derive factorization")(testFactorization(`🤫`[Foo, MapF])) + + type MapB[E, A] = Map[String, (Class[_], String)] + + def testBiFactorization(barMap: Bar[MapB]) = { + assert( + barMap.building[Int](100)(List(1, 2, 3)) === + Map( + "" -> (classOf[Bar[Any]] -> ""), + "building-res" -> (classOf[List[Any]] -> ""), + "building-err" -> (classOf[String] -> ""), + "weight" -> (classOf[Double] -> (100: Double).show), + "elems" -> (classOf[List[Int]] -> List(1, 2, 3).show), + ) + ) + + assert( + barMap.person("Oli", 26) === + Map( + "" -> (classOf[Bar[Any]] -> ""), + "person-err" -> (classOf[Nothing] -> ""), + "person-res" -> (classOf[Unit] -> ""), + "name" -> (classOf[String] -> "Oli"), + "age" -> (classOf[Int] -> "26"), + ) + ) + } + + test("simple bifactorization")(testBiFactorization(derived.bifactorize[BiBuilder.type, MapB, Bar](BiBuilder))) + + import FactorizeSuite.{BiBuilder => `frightening and inappropriate name`} + import `frightening and inappropriate name`.{conjure => `😨`} + test("derive bifactorization")(testBiFactorization(`😨`[Bar, MapB])) + +} + +object FactorizeSuite { + type MyMap = Map[String, (Class[_], String)] + + trait Foo[F[_]] { + def person(name: String, age: Int): F[Unit] + def building[A: Show](weight: Double)(elems: List[A]): F[List[Double]] + } + + trait Bar[F[_, _]] { + def person(name: String, age: Int): F[Nothing, Unit] + def building[A: Show](weight: Double)(elems: List[A]): F[String, List[Double]] + } + + object Builder { + type Result[A] = MyMap + + def prepare[Alg[_[_]]](implicit Alg: ClassTag[Alg[Any]]) = new Builder(Alg.runtimeClass) + + def conjure[U[f[_]], F[_]]: U[F] = macro HigherKindedMacros.factorizeThis[U] + } + + class Builder(algCls: Class[_]) { + def start[Res: ClassTag](name: String): Building = + Building(Map(name -> (classTag[Res].runtimeClass -> ""), "" -> (algCls -> ""))) + } + + object BiBuilder { + type Result[E, A] = MyMap + + def prepare[Alg[_[_, _]]](implicit Alg: ClassTag[Alg[Any]]) = new BiBuilder(Alg.runtimeClass) + + def conjure[U[f[_, _]], F[_, _]]: U[F] = macro HigherKindedMacros.bifactorizeThis[U] + } + + class BiBuilder(algCls: Class[_]) { + def start[Err: ClassTag, Res: ClassTag](name: String): Building = + Building( + Map( + s"$name-err" -> (classTag[Err].runtimeClass -> ""), + s"$name-res" -> (classTag[Res].runtimeClass -> ""), + "" -> (algCls -> "") + ) + ) + } + + case class Building(result: Map[String, (Class[_], String)] = Map()) { + def arg[V: ClassTag: Show](name: String, a: V): Building = + copy(result = result + (name -> (classTag[V].runtimeClass -> a.show))) + } +} diff --git a/logging/derivation/src/main/scala/tofu/logging/derivation/loggingMid.scala b/logging/derivation/src/main/scala/tofu/logging/derivation/loggingMid.scala new file mode 100644 index 000000000..3f67ebc85 --- /dev/null +++ b/logging/derivation/src/main/scala/tofu/logging/derivation/loggingMid.scala @@ -0,0 +1,58 @@ +package tofu.logging +package derivation + +import tofu.higherKind.derived.HigherKindedMacros +import derevo.DerivationKN3 +import derevo.DerivationKN11 +import tofu.logging.bi.LoggingBiMid +import derevo.PassTypeArgs +import derevo.ParamRequire +import tofu.logging.builder.LoggingMidBuilder +import tofu.logging.builder.LoggingErrMidBuilder +import tofu.logging.builder.LoggingBiMidBuilder + +/** Default logging derivation mechanism for unary effect algebras + * + * Adds logging around the successful invocation of each method at DEBUG level. + * + * @note Class name is not printed by default. + * + * For customization create an object with the same parents and abstract type member `Result` + * and redefine [onEnter] and [onLeave] methods of the `LoggingMidBuilder` trait. + */ +object loggingMid + extends LoggingMidBuilder.Default with DerivationKN3[LoggingMid.Of] with PassTypeArgs with ParamRequire[Loggable] { + type Result[A] = LoggingMid[A] + def instance[U[f[_]]]: U[LoggingMid] = macro HigherKindedMacros.factorizeThis[U] +} + +/** Default logging derivation mechanism for unary effect algebras with error reporting. + * + * Adds logging around the invocation of each method at DEBUG level and error alert at ERROR level + * @note Class name is not printed by default. + * + * For customization create object with the same parents and abstract type member `Result` + * and redefine [onEnter], [onLeave] and [onFault] methods of the `LoggingErrMidBuilder` trait. + */ +object loggingMidTry + extends LoggingErrMidBuilder.DefaultImpl[Throwable] with DerivationKN3[LoggingErrMid.Try] with PassTypeArgs + with ParamRequire[Loggable] { + type Result[A] = LoggingErrMid[Throwable, A] + def instance[U[f[_]]]: U[Result] = macro HigherKindedMacros.factorizeThis[U] +} + +/** Default logging with errors derivation mechanism for binary effect algebras. + * + * Adds logging around invocation of each method at DEBUG level and error alert at ERROR level + * @note Class name is not printed by default. + * + * For customization create object with the same parents and abstract type member `Result` + * and redefine [onEnter], [onLeave] methods of the `LoggingBiMidBuilder` trait. + */ +object loggingBiMid + extends LoggingBiMidBuilder.Default with DerivationKN11[LoggingBiMid.Of] with PassTypeArgs + with ParamRequire[Loggable] { + type Result[E, A] = LoggingBiMid[E, A] + def instance[U[f[_, _]]]: U[LoggingBiMid] = + macro HigherKindedMacros.bifactorizeThis[U] +} diff --git a/logging/derivation/src/test/scala/tofu/logging/derivation/LoggingMidSuite.scala b/logging/derivation/src/test/scala/tofu/logging/derivation/LoggingMidSuite.scala new file mode 100644 index 000000000..93e888dc0 --- /dev/null +++ b/logging/derivation/src/test/scala/tofu/logging/derivation/LoggingMidSuite.scala @@ -0,0 +1,81 @@ +package tofu.logging +package derivation + +import derevo.derive +import org.scalatest.funsuite.AnyFunSuite +import org.slf4j.helpers.MessageFormatter +import tofu.data._ +import tofu.higherKind.derived.representableK +import tofu.optics.macros.Optics + +import LoggingMidSuite._ + +@derive(representableK, loggingMidTry) +trait Greeter[F[_]] { + def setName(name: String): F[Unit] + def hello: F[String] +} + +object Greeter extends LoggingCompanion[Greeter] { + import LoggingMidSuite._ + implicit object Instance extends Greeter[Eff] { + def setName(name: String): Eff[Unit] = + CalcM.set(Some(name)).focus(State.name).void + + def hello: Eff[String] = CalcM.get[State].map(_.name).flatMap { + case None => CalcM.raise(MissingName()) + case Some(name) => CalcM.pure(s"Hello, $name") + } + } +} + +object LoggingMidSuite { + + case class MissingName() extends Throwable + + @Optics + case class State(name: Option[String] = None, logs: Vector[String] = Vector()) + + type Eff[+A] = ICalcM[Nothing2T, Any, State, Throwable, A] + + def logging(name: String): Logging[Eff] = new Logging[Eff] { + private def put(message: String)(logs: Vector[String]) = logs :+ message + def write(level: Logging.Level, message: String, values: LoggedValue*): Eff[Unit] = { + val interpolated = MessageFormatter.arrayFormat(message, values.toArray).getMessage() + CalcM.update(put(s"[$level] <$name> $interpolated")).void.focus(State.logs) + } + } + + implicit val logs: Logs.Universal[Eff] = logging +} + +class LoggingMidSuite extends AnyFunSuite { + val greeter: Greeter[Eff] = Greeter.Instance.attachErrLogs[Throwable] + val ErrName = classOf[MissingName].getName() + val GreeterName = classOf[Greeter[Any]].getName() + + test("should raise an error when instance is not set") { + val (State(_, logs), result) = greeter.hello.runUnit(State()) + assert(result === Left(MissingName())) + assert( + logs === Vector( + s"[Debug] <$GreeterName> entering hello ()", + s"[Error] <$GreeterName> error during hello () error is $ErrName" + ) + ) + } + + test("should succesfully read name with name is set") { + val (State(_, logs), result) = (greeter.setName("Tofu") >> greeter.hello).runUnit(State()) + assert(result === Right("Hello, Tofu")) + + assert( + logs === Vector( + s"[Debug] <$GreeterName> entering setName (name = Tofu)", + s"[Debug] <$GreeterName> leaving setName (name = Tofu) with result ()", + s"[Debug] <$GreeterName> entering hello ()", + s"[Debug] <$GreeterName> leaving hello () with result Hello, Tofu" + ) + ) + } +} diff --git a/logging/structured/src/main/scala/tofu/logging/Loggable.scala b/logging/structured/src/main/scala/tofu/logging/Loggable.scala index 5da0e0112..4593f4540 100644 --- a/logging/structured/src/main/scala/tofu/logging/Loggable.scala +++ b/logging/structured/src/main/scala/tofu/logging/Loggable.scala @@ -1,34 +1,17 @@ package tofu.logging -import java.io.{PrintWriter, StringWriter} -import java.time.{Instant, LocalDate, LocalDateTime, OffsetDateTime, ZonedDateTime} -import java.util.UUID - -import alleycats.std.iterable._ -import alleycats.std.set._ -import cats.data._ -import cats.instances.list._ -import cats.instances.map._ -import cats.instances.sortedSet._ -import cats.instances.string._ -import cats.instances.vector._ -import cats.syntax.foldable._ -import cats.syntax.monoid._ +import scala.annotation.nowarn + +import scala.{PartialFunction => PF, specialized => sp} + import cats.syntax.show._ -import cats.{Foldable, Show} +import cats.{Show} import simulacrum.typeclass + import tofu.control.Consume import tofu.logging.Loggable.Base import tofu.logging.impl._ import tofu.syntax.logRenderer._ -import tofu.compat._ -import tofu.compat.lazySeqInstances._ - -import scala.annotation.nowarn -import scala.collection.immutable.SortedSet -import scala.collection.{immutable, mutable} -import scala.concurrent.duration.FiniteDuration -import scala.{PartialFunction => PF, specialized => sp} /** Typeclass for adding custom log values to message */ @@ -59,7 +42,7 @@ trait Loggable[A] extends Loggable.Base[A] { def narrow[B <: A]: Loggable[B] = this.asInstanceOf[Loggable[B]] } -object Loggable { +object Loggable extends LoggableInstances { /** contravariant version of `Loggable` if one need it */ @typeclass @nowarn("cat=unused-imports") @@ -108,9 +91,9 @@ object Loggable { override def putField[I, V, R, S](i: I, name: String)(implicit r: LogRenderer[I, V, R, S]): R = self.putField(a, name, i) - override def toString: String = logShow(a) - override def typeName: String = self.typeName - def shortName: String = self.shortName + override def toString: String = logShow(a) + override def typeName: String = self.typeName + override def shortName: String = self.shortName } /** transform input value befor logging */ @@ -151,156 +134,13 @@ object Loggable { Loggable[A].contraCollect[Either[A, B]] { case Left(a) => a } + Loggable[B].contraCollect { case Right(b) => b } - final implicit val stringValue: Loggable[String] = new SingleValueLoggable[String] { - def logValue(a: String): LogParamValue = StrValue(a) - override def putField[I, V, R, M](a: String, name: String, input: I)(implicit - receiver: LogRenderer[I, V, R, M] - ): R = - receiver.addString(name, a, input) - } - - final implicit val byteLoggable: Loggable[Byte] = new SingleValueLoggable[Byte] { - def logValue(a: Byte): LogParamValue = IntValue(a.toLong) - override def putField[I, V, R, M](a: Byte, name: String, input: I)(implicit receiver: LogRenderer[I, V, R, M]): R = - receiver.addInt(name, a.toLong, input) - } - - final implicit val shortLoggable: Loggable[Short] = new SingleValueLoggable[Short] { - def logValue(a: Short): LogParamValue = IntValue(a.toLong) - override def putField[I, V, R, M](a: Short, name: String, input: I)(implicit receiver: LogRenderer[I, V, R, M]): R = - receiver.addInt(name, a.toLong, input) - } - - final implicit val intLoggable: Loggable[Int] = new SingleValueLoggable[Int] { - def logValue(a: Int): LogParamValue = IntValue(a.toLong) - override def putField[I, V, R, M](a: Int, name: String, input: I)(implicit receiver: LogRenderer[I, V, R, M]): R = - receiver.addInt(name, a.toLong, input) - } - - final implicit val longLoggable: Loggable[Long] = new SingleValueLoggable[Long] { - def logValue(a: Long): LogParamValue = IntValue(a) - override def putField[I, V, R, M](a: Long, name: String, input: I)(implicit receiver: LogRenderer[I, V, R, M]): R = - receiver.addInt(name, a, input) - } - - final implicit val bigIngLoggable: Loggable[BigInt] = new SingleValueLoggable[BigInt] { - def logValue(a: BigInt): LogParamValue = BigIntValue(a) - override def putField[I, V, R, M](a: BigInt, name: String, input: I)(implicit - receiver: LogRenderer[I, V, R, M] - ): R = - receiver.addBigInt(name, a, input) - } - - final implicit val bigDecimalLoggable: Loggable[BigDecimal] = new SingleValueLoggable[BigDecimal] { - def logValue(a: BigDecimal): LogParamValue = DecimalValue(a) - override def putField[I, V, R, M](a: BigDecimal, name: String, input: I)(implicit - receiver: LogRenderer[I, V, R, M] - ): R = - receiver.addDecimal(name, a, input) - } - - final implicit val floatLoggable: Loggable[Float] = new SingleValueLoggable[Float] { - def logValue(a: Float): LogParamValue = FloatValue(a.toDouble) - override def putField[I, V, R, M](a: Float, name: String, input: I)(implicit receiver: LogRenderer[I, V, R, M]): R = - receiver.addFloat(name, a.toDouble, input) - } - - final implicit val doubleLoggable: Loggable[Double] = new SingleValueLoggable[Double] { - def logValue(a: Double): LogParamValue = FloatValue(a) - override def putField[I, V, R, M](a: Double, name: String, input: I)(implicit - receiver: LogRenderer[I, V, R, M] - ): R = - receiver.addFloat(name, a, input) - } - - final implicit val booleanLoggable: Loggable[Boolean] = new SingleValueLoggable[Boolean] { - def logValue(a: Boolean): LogParamValue = BoolValue(a) - override def putField[I, V, R, M](a: Boolean, name: String, input: I)(implicit - receiver: LogRenderer[I, V, R, M] - ): R = - receiver.addBool(name, a, input) - } - - private[this] def fldLoggable[T[x]: Foldable, A](implicit A: Loggable[A]): Loggable[T[A]] = - new SubLoggable[T[A]] { - def putValue[I, V, R, M](ta: T[A], v: V)(implicit r: LogRenderer[I, V, R, M]): M = { - val arr = ta.foldLeft(mutable.Buffer.newBuilder[A])(_ += _).result() - v.list(arr.size)((v, idx) => A.putValue(arr(idx), v)) - } - def logShow(a: T[A]): String = { - implicit val show: Show[A] = A.showInstance - a.mkString_("[", ",", "]") - } - } - final implicit def seqLoggable[A: Loggable]: Loggable[collection.Seq[A]] = - fldLoggable[Iterable, A].contramap(_.toIterable) - final implicit def immutableSeqLoggable[A: Loggable]: Loggable[immutable.Seq[A]] = - fldLoggable[Iterable, A].contramap(_.toIterable) - - final implicit def listLoggable[A: Loggable]: Loggable[List[A]] = fldLoggable[List, A] - final implicit def vectorLoggable[A: Loggable]: Loggable[Vector[A]] = fldLoggable[Vector, A] - final implicit def streamLoggable[A: Loggable]: Loggable[LazySeq[A]] = fldLoggable[LazySeq, A] - final implicit def chainLoggable[A: Loggable]: Loggable[Chain[A]] = fldLoggable[Chain, A] - final implicit def setLoggable[A: Loggable]: Loggable[Set[A]] = fldLoggable[Set, A] - final implicit def sortedSetLoggable[A: Loggable]: Loggable[SortedSet[A]] = fldLoggable[SortedSet, A] - - final implicit def nonEmptyListLoggable[A: Loggable]: Loggable[NonEmptyList[A]] = fldLoggable[NonEmptyList, A] - final implicit def nonEmptyVectorLoggable[A: Loggable]: Loggable[NonEmptyVector[A]] = fldLoggable[NonEmptyVector, A] - final implicit def nonEmptyStreamLoggable[A: Loggable]: Loggable[NELazySeq[A]] = fldLoggable[NELazySeq, A] - final implicit def nonEmptyChainLoggable[A: Loggable]: Loggable[NonEmptyChain[A]] = fldLoggable[NonEmptyChain, A] - final implicit def nonEmptySetLoggable[A: Loggable]: Loggable[NonEmptySet[A]] = fldLoggable[NonEmptySet, A] - - final implicit val instantLoggable: Loggable[Instant] = stringValue.contramap(_.toString) - final implicit val zonedDateTimeLoggable: Loggable[ZonedDateTime] = stringValue.contramap(_.toString) - final implicit val offsetDateTimeLoggable: Loggable[OffsetDateTime] = stringValue.contramap(_.toString) - final implicit val localDateTimeLoggable: Loggable[LocalDateTime] = stringValue.contramap(_.toString) - final implicit val localDateLoggable: Loggable[LocalDate] = stringValue.contramap(_.toString) - final implicit val durationLoggable: Loggable[java.time.Duration] = stringValue.contramap(_.toString) - final implicit val uuidLoggable: Loggable[UUID] = stringValue.contramap(_.toString) - final implicit val finiteDurationLoggable: Loggable[FiniteDuration] = stringValue.contramap(_.toString) - final implicit val sqlDateLoggable: Loggable[java.sql.Date] = stringValue.contramap(_.toString) - final implicit val sqlTimeLoggable: Loggable[java.sql.Time] = stringValue.contramap(_.toString) - final implicit val sqlTimestampLoggable: Loggable[java.sql.Timestamp] = stringValue.contramap(_.toString) - - final implicit def mapLoggable[A](implicit A: Loggable[A]): Loggable[Map[String, A]] = - new DictLoggable[Map[String, A]] { - implicit val ashow: Show[A] = A.showInstance - def fields[I, V, R, M](a: Map[String, A], i: I)(implicit r: LogRenderer[I, V, R, M]): R = - a.foldLeft(i.noop)((acc, kv) => acc |+| A.putField(kv._2, kv._1, i)) - def logShow(a: Map[String, A]): String = a.show - } - - implicit val loggableInstance: Consume[Loggable] = new Consume[Loggable] { + final implicit val loggableInstance: Consume[Loggable] = new Consume[Loggable] { def empty[A]: Loggable[A] = Loggable.empty def combineK[A](x: Loggable[A], y: Loggable[A]): Loggable[A] = x + y def contramap[A, B](fa: Loggable[A])(f: B => A): Loggable[B] = fa.contramap(f) def switch[A, B](fa: Loggable[A], fb: Loggable[B]): Loggable[Either[A, B]] = Loggable.either[A, B](fa, fb) } - - final implicit def optLoggable[T](implicit loggable: Loggable[T]): Loggable[Option[T]] = new Loggable[Option[T]] { - override def logVia(value: Option[T], f: (String, Any) => Unit): Unit = value.foreach(loggable.logVia(_, f)) - - def putValue[I, V, R, M](oa: Option[T], v: V)(implicit r: LogRenderer[I, V, R, M]): M = oa match { - case None => v.zero - case Some(a) => loggable.putValue(a, v) - } - - def fields[I, V, R, @sp(Unit) M](oa: Option[T], i: I)(implicit receiver: LogRenderer[I, V, R, M]): R = oa match { - case None => i.noop - case Some(a) => loggable.fields(a, i) - } - - override def putField[I, V, R, M](oa: Option[T], name: String, input: I)(implicit - receiver: LogRenderer[I, V, R, M] - ): R = - oa match { - case None => input.noop - case Some(a) => loggable.putField(a, name, input) - } - - def logShow(a: Option[T]): String = a.fold("")(loggable.logShow) - } } /** specialized Loggable for multi-field objects */ @@ -339,46 +179,3 @@ trait SingleValueLoggable[@specialized A] extends Loggable[A] with SubLoggable[A override def putValue[I, V, R, M](a: A, v: V)(implicit r: LogRenderer[I, V, R, M]): M = r.putValue(logValue(a), v) } - -trait LoggedValue { - def typeName: String - def shortName: String - - def logFields[I, V, @sp(Unit) R, @sp M](input: I)(implicit r: LogRenderer[I, V, R, M]): R - def putValue[I, V, R, S](v: V)(implicit r: LogRenderer[I, V, R, S]): S = r.dict(v)(logFields(_)) - def putField[I, V, R, S](i: I, name: String)(implicit r: LogRenderer[I, V, R, S]): R = r.sub(name, i)(putValue(_)) - - def foreachLog(f: (String, Any) => Unit): Unit = - logFields("")(LogRenderer.prefixed(f)) -} - -object LoggedValue { - implicit val loggable: Loggable[LoggedValue] = new Loggable[LoggedValue] { - def fields[I, V, @sp(Unit) R, M](a: LoggedValue, input: I)(implicit receiver: LogRenderer[I, V, R, M]): R = - a.logFields(input) - - override def putValue[I, V, R, S](a: LoggedValue, v: V)(implicit r: LogRenderer[I, V, R, S]): S = a.putValue(v) - - override def putField[I, V, R, S](a: LoggedValue, name: String, i: I)(implicit r: LogRenderer[I, V, R, S]): R = - a.putField(i, name) - - def logShow(a: LoggedValue): String = a.toString - } - - implicit def loggableToLoggedValue[A](x: A)(implicit loggable: Loggable[A]): LoggedValue = loggable.loggedValue(x) - - def error(cause: Throwable): LoggedThrowable = new LoggedThrowable(cause) -} - -final class LoggedThrowable(cause: Throwable) extends Throwable(cause.getMessage, cause) with LoggedValue { - override def toString: String = cause.toString - - def logFields[I, V, @sp(Unit) R, @sp M](input: I)(implicit f: LogRenderer[I, V, R, M]): R = { - val strWriter = new StringWriter() - cause.printStackTrace(new PrintWriter(strWriter)) - f.addString("stacktrace", strWriter.toString, input) - } - - override def typeName: String = cause.getClass.getTypeName - def shortName: String = "exception" -} diff --git a/logging/structured/src/main/scala/tofu/logging/LoggableInstances.scala b/logging/structured/src/main/scala/tofu/logging/LoggableInstances.scala new file mode 100644 index 000000000..c856bc197 --- /dev/null +++ b/logging/structured/src/main/scala/tofu/logging/LoggableInstances.scala @@ -0,0 +1,192 @@ +package tofu.logging + +import java.time.{Instant, LocalDate, LocalDateTime, OffsetDateTime, ZonedDateTime} +import java.util.UUID + +import scala.collection.immutable.SortedSet +import scala.collection.{immutable, mutable} +import scala.concurrent.duration.FiniteDuration +import scala.{specialized => sp} + +import alleycats.std.iterable._ +import alleycats.std.set._ +import cats.data._ +import cats.instances.list._ +import cats.instances.map._ +import cats.instances.sortedSet._ +import cats.instances.string._ +import cats.instances.vector._ +import cats.syntax.foldable._ +import cats.syntax.monoid._ +import cats.syntax.show._ +import cats.{Foldable, Show} +import tofu.compat._ +import tofu.compat.lazySeqInstances._ +import tofu.syntax.logRenderer._ +import java.io.StringWriter +import java.io.PrintWriter + +class LoggableInstances { + final implicit val stringValue: Loggable[String] = new SingleValueLoggable[String] { + def logValue(a: String): LogParamValue = StrValue(a) + override def putField[I, V, R, M](a: String, name: String, input: I)(implicit + receiver: LogRenderer[I, V, R, M] + ): R = + receiver.addString(name, a, input) + } + + final implicit val byteLoggable: Loggable[Byte] = new SingleValueLoggable[Byte] { + def logValue(a: Byte): LogParamValue = IntValue(a.toLong) + override def putField[I, V, R, M](a: Byte, name: String, input: I)(implicit receiver: LogRenderer[I, V, R, M]): R = + receiver.addInt(name, a.toLong, input) + } + + final implicit val shortLoggable: Loggable[Short] = new SingleValueLoggable[Short] { + def logValue(a: Short): LogParamValue = IntValue(a.toLong) + override def putField[I, V, R, M](a: Short, name: String, input: I)(implicit receiver: LogRenderer[I, V, R, M]): R = + receiver.addInt(name, a.toLong, input) + } + + final implicit val intLoggable: Loggable[Int] = new SingleValueLoggable[Int] { + def logValue(a: Int): LogParamValue = IntValue(a.toLong) + override def putField[I, V, R, M](a: Int, name: String, input: I)(implicit receiver: LogRenderer[I, V, R, M]): R = + receiver.addInt(name, a.toLong, input) + } + + final implicit val longLoggable: Loggable[Long] = new SingleValueLoggable[Long] { + def logValue(a: Long): LogParamValue = IntValue(a) + override def putField[I, V, R, M](a: Long, name: String, input: I)(implicit receiver: LogRenderer[I, V, R, M]): R = + receiver.addInt(name, a, input) + } + + final implicit val bigIngLoggable: Loggable[BigInt] = new SingleValueLoggable[BigInt] { + def logValue(a: BigInt): LogParamValue = BigIntValue(a) + override def putField[I, V, R, M](a: BigInt, name: String, input: I)(implicit + receiver: LogRenderer[I, V, R, M] + ): R = + receiver.addBigInt(name, a, input) + } + + final implicit val bigDecimalLoggable: Loggable[BigDecimal] = new SingleValueLoggable[BigDecimal] { + def logValue(a: BigDecimal): LogParamValue = DecimalValue(a) + override def putField[I, V, R, M](a: BigDecimal, name: String, input: I)(implicit + receiver: LogRenderer[I, V, R, M] + ): R = + receiver.addDecimal(name, a, input) + } + + final implicit val floatLoggable: Loggable[Float] = new SingleValueLoggable[Float] { + def logValue(a: Float): LogParamValue = FloatValue(a.toDouble) + override def putField[I, V, R, M](a: Float, name: String, input: I)(implicit receiver: LogRenderer[I, V, R, M]): R = + receiver.addFloat(name, a.toDouble, input) + } + + final implicit val doubleLoggable: Loggable[Double] = new SingleValueLoggable[Double] { + def logValue(a: Double): LogParamValue = FloatValue(a) + override def putField[I, V, R, M](a: Double, name: String, input: I)(implicit + receiver: LogRenderer[I, V, R, M] + ): R = + receiver.addFloat(name, a, input) + } + + final implicit val booleanLoggable: Loggable[Boolean] = new SingleValueLoggable[Boolean] { + def logValue(a: Boolean): LogParamValue = BoolValue(a) + override def putField[I, V, R, M](a: Boolean, name: String, input: I)(implicit + receiver: LogRenderer[I, V, R, M] + ): R = + receiver.addBool(name, a, input) + } + + final implicit val unitLoggable: Loggable[Unit] = new SingleValueLoggable[Unit] { + def logValue(a: Unit): LogParamValue = NullValue + override def putField[I, V, R, M](a: Unit, name: String, input: I)(implicit + receiver: LogRenderer[I, V, R, M] + ): R = receiver.noop(input) + } + + final implicit val throwableLoggable: Loggable[Throwable] = new Loggable[Throwable] { + override def fields[I, V, R, S](cause: Throwable, i: I)(implicit r: LogRenderer[I, V, R, S]): R = { + val strWriter = new StringWriter() + cause.printStackTrace(new PrintWriter(strWriter)) + r.addString("stacktrace", strWriter.toString, i) + } + + override def putValue[I, V, R, S](a: Throwable, v: V)(implicit r: LogRenderer[I, V, R, S]): S = + r.putString(a.toString(), v) + + override def logShow(a: Throwable): String = a.toString + } + + private[this] def fldLoggable[T[x]: Foldable, A](implicit A: Loggable[A]): Loggable[T[A]] = + new SubLoggable[T[A]] { + def putValue[I, V, R, M](ta: T[A], v: V)(implicit r: LogRenderer[I, V, R, M]): M = { + val arr = ta.foldLeft(mutable.Buffer.newBuilder[A])(_ += _).result() + v.list(arr.size)((v, idx) => A.putValue(arr(idx), v)) + } + def logShow(a: T[A]): String = { + implicit val show: Show[A] = A.showInstance + a.mkString_("[", ",", "]") + } + } + final implicit def seqLoggable[A: Loggable]: Loggable[collection.Seq[A]] = + fldLoggable[Iterable, A].contramap(_.toIterable) + final implicit def immutableSeqLoggable[A: Loggable]: Loggable[immutable.Seq[A]] = + fldLoggable[Iterable, A].contramap(_.toIterable) + + final implicit def listLoggable[A: Loggable]: Loggable[List[A]] = fldLoggable[List, A] + final implicit def vectorLoggable[A: Loggable]: Loggable[Vector[A]] = fldLoggable[Vector, A] + final implicit def streamLoggable[A: Loggable]: Loggable[LazySeq[A]] = fldLoggable[LazySeq, A] + final implicit def chainLoggable[A: Loggable]: Loggable[Chain[A]] = fldLoggable[Chain, A] + final implicit def setLoggable[A: Loggable]: Loggable[Set[A]] = fldLoggable[Set, A] + final implicit def sortedSetLoggable[A: Loggable]: Loggable[SortedSet[A]] = fldLoggable[SortedSet, A] + + final implicit def nonEmptyListLoggable[A: Loggable]: Loggable[NonEmptyList[A]] = fldLoggable[NonEmptyList, A] + final implicit def nonEmptyVectorLoggable[A: Loggable]: Loggable[NonEmptyVector[A]] = fldLoggable[NonEmptyVector, A] + final implicit def nonEmptyStreamLoggable[A: Loggable]: Loggable[NELazySeq[A]] = fldLoggable[NELazySeq, A] + final implicit def nonEmptyChainLoggable[A: Loggable]: Loggable[NonEmptyChain[A]] = fldLoggable[NonEmptyChain, A] + final implicit def nonEmptySetLoggable[A: Loggable]: Loggable[NonEmptySet[A]] = fldLoggable[NonEmptySet, A] + + final implicit val instantLoggable: Loggable[Instant] = stringValue.contramap(_.toString) + final implicit val zonedDateTimeLoggable: Loggable[ZonedDateTime] = stringValue.contramap(_.toString) + final implicit val offsetDateTimeLoggable: Loggable[OffsetDateTime] = stringValue.contramap(_.toString) + final implicit val localDateTimeLoggable: Loggable[LocalDateTime] = stringValue.contramap(_.toString) + final implicit val localDateLoggable: Loggable[LocalDate] = stringValue.contramap(_.toString) + final implicit val durationLoggable: Loggable[java.time.Duration] = stringValue.contramap(_.toString) + final implicit val uuidLoggable: Loggable[UUID] = stringValue.contramap(_.toString) + final implicit val finiteDurationLoggable: Loggable[FiniteDuration] = stringValue.contramap(_.toString) + final implicit val sqlDateLoggable: Loggable[java.sql.Date] = stringValue.contramap(_.toString) + final implicit val sqlTimeLoggable: Loggable[java.sql.Time] = stringValue.contramap(_.toString) + final implicit val sqlTimestampLoggable: Loggable[java.sql.Timestamp] = stringValue.contramap(_.toString) + + final implicit def mapLoggable[A](implicit A: Loggable[A]): Loggable[Map[String, A]] = + new DictLoggable[Map[String, A]] { + implicit val ashow: Show[A] = A.showInstance + def fields[I, V, R, M](a: Map[String, A], i: I)(implicit r: LogRenderer[I, V, R, M]): R = + a.foldLeft(i.noop)((acc, kv) => acc |+| A.putField(kv._2, kv._1, i)) + def logShow(a: Map[String, A]): String = a.show + } + + final implicit def optLoggable[T](implicit loggable: Loggable[T]): Loggable[Option[T]] = new Loggable[Option[T]] { + override def logVia(value: Option[T], f: (String, Any) => Unit): Unit = value.foreach(loggable.logVia(_, f)) + + def putValue[I, V, R, M](oa: Option[T], v: V)(implicit r: LogRenderer[I, V, R, M]): M = oa match { + case None => v.zero + case Some(a) => loggable.putValue(a, v) + } + + def fields[I, V, R, @sp(Unit) M](oa: Option[T], i: I)(implicit receiver: LogRenderer[I, V, R, M]): R = oa match { + case None => i.noop + case Some(a) => loggable.fields(a, i) + } + + override def putField[I, V, R, M](oa: Option[T], name: String, input: I)(implicit + receiver: LogRenderer[I, V, R, M] + ): R = + oa match { + case None => input.noop + case Some(a) => loggable.putField(a, name, input) + } + + def logShow(a: Option[T]): String = a.fold("")(loggable.logShow) + } +} diff --git a/logging/structured/src/main/scala/tofu/logging/LoggedValue.scala b/logging/structured/src/main/scala/tofu/logging/LoggedValue.scala new file mode 100644 index 000000000..93d1e2081 --- /dev/null +++ b/logging/structured/src/main/scala/tofu/logging/LoggedValue.scala @@ -0,0 +1,43 @@ +package tofu.logging + +import scala.{specialized => sp} + +trait LoggedValue { + def typeName: String = "" + def shortName: String = "" + + def logFields[I, V, @sp(Unit) R, @sp M](input: I)(implicit r: LogRenderer[I, V, R, M]): R + def putValue[I, V, R, S](v: V)(implicit r: LogRenderer[I, V, R, S]): S = r.dict(v)(logFields(_)) + def putField[I, V, R, S](i: I, name: String)(implicit r: LogRenderer[I, V, R, S]): R = r.sub(name, i)(putValue(_)) + + def foreachLog(f: (String, Any) => Unit): Unit = + logFields("")(LogRenderer.prefixed(f)) +} + +object LoggedValue { + implicit val loggable: Loggable[LoggedValue] = new Loggable[LoggedValue] { + def fields[I, V, @sp(Unit) R, M](a: LoggedValue, input: I)(implicit receiver: LogRenderer[I, V, R, M]): R = + a.logFields(input) + + override def putValue[I, V, R, S](a: LoggedValue, v: V)(implicit r: LogRenderer[I, V, R, S]): S = a.putValue(v) + + override def putField[I, V, R, S](a: LoggedValue, name: String, i: I)(implicit r: LogRenderer[I, V, R, S]): R = + a.putField(i, name) + + def logShow(a: LoggedValue): String = a.toString + } + + implicit def loggableToLoggedValue[A](x: A)(implicit loggable: Loggable[A]): LoggedValue = loggable.loggedValue(x) + + def error(cause: Throwable): LoggedThrowable = new LoggedThrowable(cause) +} + +final class LoggedThrowable(cause: Throwable) extends Throwable(cause.getMessage, cause) with LoggedValue { + override def toString: String = cause.toString + + def logFields[I, V, @sp(Unit) R, @sp M](input: I)(implicit f: LogRenderer[I, V, R, M]): R = + Loggable.throwableLoggable.fields(cause, input) + + override def typeName: String = cause.getClass.getTypeName + override def shortName: String = "exception" +} diff --git a/logging/structured/src/main/scala/tofu/logging/Logging.scala b/logging/structured/src/main/scala/tofu/logging/Logging.scala index 9a98d662d..5eff7b369 100644 --- a/logging/structured/src/main/scala/tofu/logging/Logging.scala +++ b/logging/structured/src/main/scala/tofu/logging/Logging.scala @@ -1,18 +1,18 @@ package tofu.logging +import scala.reflect.ClassTag + import cats.kernel.Monoid -import cats.syntax.apply._ import cats.{Applicative, Apply, FlatMap} import org.slf4j.{Logger, LoggerFactory, Marker} import tofu.compat.unused import tofu.higherKind.{Function2K, RepresentableK} -import tofu.logging.Logging._ +import tofu.logging.Logging.{Debug, Error, Info, Level, Trace, Warn} import tofu.logging.impl.EmbedLogging +import tofu.syntax.monadic._ import tofu.syntax.monoidalK._ import tofu.{Init, higherKind} -import scala.reflect.ClassTag - /** Typeclass equivalent of Logger. * May contain specified some Logger instance * or try to read it from the context @@ -63,6 +63,8 @@ trait LoggingBase[F[_]] { writeCause(Warn, message, cause, values: _*) def errorCause(message: String, cause: Throwable, values: LoggedValue*): F[Unit] = writeCause(Error, message, cause, values: _*) + + def asLogging: Logging[F] } /** Logging tagged with some arbitrary tag type. @@ -94,11 +96,19 @@ object ServiceLogging { */ trait Logging[F[_]] extends ServiceLogging[F, Nothing] { final def widen[G[a] >: F[a]]: Logging[G] = this.asInstanceOf[Logging[G]] + + def asLogging: Logging[F] = this } object Logging { + + def mid: LoggingMidFunctions.type = LoggingMidFunctions + type ForService[F[_], Svc] <: Logging[F] + type Safe[F[_, _]] = Logging[F[Nothing, *]] + type SafeBase[F[_, _]] = LoggingBase[F[Nothing, *]] + def apply[F[_]](implicit logging: Logging[F]): Logging[F] = logging /** the do-nothing Logging */ @@ -134,14 +144,3 @@ private[tofu] class EmptyLogging[F[_]: Applicative] extends Logging[F] { private[this] val noop = Applicative[F].unit def write(level: Level, message: String, values: LoggedValue*): F[Unit] = noop } - -/** Mix-in trait that supposed to be extended by companion of service - * - * @example {{{ - * class FooService[F[_] : FooService.Log] - * object FooService extends LoggingCompanion[FooService] - * }}} - */ -trait LoggingCompanion[U[_[_]]] { - type Log[F[_]] = ServiceLogging[F, U[Any]] -} diff --git a/logging/structured/src/main/scala/tofu/logging/LoggingCompanion.scala b/logging/structured/src/main/scala/tofu/logging/LoggingCompanion.scala new file mode 100644 index 000000000..9c73600ab --- /dev/null +++ b/logging/structured/src/main/scala/tofu/logging/LoggingCompanion.scala @@ -0,0 +1,170 @@ +package tofu +package logging + +import scala.reflect.ClassTag + +import cats.tagless.FunctorK +import cats.{Functor, Id, Monad} +import tofu.higherKind.{Function2K, Mid, MonoidalK} +import tofu.syntax.monadic._ +import tofu.syntax.monoidalK._ + +/** Mix-in trait that supposed to be extended by companion of service + * + * @example {{{ + * class FooService[F[_] : FooService.Log] + * object FooService extends LoggingCompanion[FooService] + * }}} + */ +trait LoggingCompanion[U[_[_]]] { + type Log[F[_]] = ServiceLogging[F, U[Any]] + + implicit def toLogMidOps[F[_]](uf: U[F]): LogMidOps[U, F] = new LogMidOps(uf) + + def logMid[F[_]: Monad](implicit + L: Logs.Universal[F], + svc: ClassTag[U[F]], + U: FunctorK[U], + UM: LoggingMid.Of[U] + ): U[Mid[F, *]] = Logging.mid.in[U, Id, F] + + def logMidNamed[F[_]: Monad](name: String)(implicit + L: Logs.Universal[F], + U: FunctorK[U], + UM: LoggingMid.Of[U] + ): U[Mid[F, *]] = Logging.mid.named[U, Id, F](name) + + def logErrMid[F[+_]: Monad, E](implicit + logs: Logs.Universal[F], + UCls: ClassTag[U[F]], + lmid: LoggingErrMid.Of[U, E], + errs: Errors[F, E], + U: FunctorK[U], + ): U[Mid[F, *]] = Logging.mid.errIn[U, Id, F, E] + + def logErrMidNamed[F[+_]: Monad, E](name: String)(implicit + logs: Logs.Universal[F], + lmid: LoggingErrMid.Of[U, E], + errs: Errors[F, E], + U: FunctorK[U], + ): U[Mid[F, *]] = Logging.mid.namedErr[U, Id, F, E](name) + + def logMidIn[I[_]: Functor, F[_]: Monad](implicit + L: Logs[I, F], + svc: ClassTag[U[F]], + U: FunctorK[U], + UM: LoggingMid.Of[U] + ): I[U[Mid[F, *]]] = Logging.mid.in[U, I, F] + + def logMidNamedIn[I[_]: Functor, F[_]: Monad](name: String)(implicit + L: Logs[I, F], + U: FunctorK[U], + UM: LoggingMid.Of[U] + ): I[U[Mid[F, *]]] = Logging.mid.named[U, I, F](name) + + def logErrMidIn[I[_]: Functor, F[+_]: Monad, E](implicit + logs: Logs[I, F], + UCls: ClassTag[U[F]], + lmid: LoggingErrMid.Of[U, E], + errs: Errors[F, E], + U: FunctorK[U], + ): I[U[Mid[F, *]]] = Logging.mid.errIn[U, I, F, E] + + def logErrMidNamedIn[I[_]: Functor, F[+_]: Monad, E](name: String)(implicit + logs: Logs[I, F], + lmid: LoggingErrMid.Of[U, E], + errs: Errors[F, E], + U: FunctorK[U], + ): I[U[Mid[F, *]]] = Logging.mid.namedErr[U, I, F, E](name) + +} + +class LogMidOps[U[f[_]], F[_]](private val uf: U[F]) extends AnyVal { + def attachLogs(implicit + UL: U[LoggingMid], + cls: ClassTag[U[F]], + L: Logs.Universal[F], + U: MonoidalK[U], + F: Monad[F] + ): U[F] = { + implicit val FL: Logging[F] = L.forService[U[F]] + UL.zipWithK(uf)(Function2K.apply((l, fx) => l.around(fx))) + } + + def attachLogsNamed(name: String)(implicit + UL: U[LoggingMid], + L: Logs.Universal[F], + U: MonoidalK[U], + F: Monad[F] + ): U[F] = { + implicit val FL: Logging[F] = L.byName(name) + UL.zipWithK(uf)(Function2K.apply((l, fx) => l.around(fx))) + } + + def attachLogsIn[I[_]](implicit + I: Functor[I], + UL: U[LoggingMid], + cls: ClassTag[U[F]], + L: Logs[I, F], + U: MonoidalK[U], + F: Monad[F] + ): I[U[F]] = L.forService[U[F]].map { implicit logging => + UL.zipWithK(uf)(Function2K.apply((l, fx) => l.around(fx))) + } + + def attachLogsNamedIn[I[_]](name: String)(implicit + I: Functor[I], + UL: U[LoggingMid], + L: Logs[I, F], + U: MonoidalK[U], + F: Monad[F] + ): I[U[F]] = L.byName(name).map { implicit logging => + UL.zipWithK(uf)(Function2K.apply((l, fx) => l.around(fx))) + } + + def attachErrLogs[E](implicit + UL: U[LoggingErrMid[E, *]], + cls: ClassTag[U[F]], + L: Logs.Universal[F], + U: MonoidalK[U], + F: Monad[F], + FE: Errors[F, E], + ): U[F] = { + implicit val FL: Logging[F] = L.forService[U[F]] + UL.zipWithK(uf)(Function2K.apply((l, fx) => l.aroundErr(fx))) + } + + def attachErrLogsNamed[E](name: String)(implicit + UL: U[LoggingErrMid[E, *]], + L: Logs.Universal[F], + U: MonoidalK[U], + F: Monad[F], + FE: Errors[F, E], + ): U[F] = { + implicit val FL: Logging[F] = L.byName(name) + UL.zipWithK(uf)(Function2K.apply((l, fx) => l.aroundErr(fx))) + } + + def attachErrLogsIn[I[_], E](implicit + I: Functor[I], + UL: U[LoggingErrMid[E, *]], + cls: ClassTag[U[F]], + L: Logs[I, F], + U: MonoidalK[U], + F: Monad[F], + FE: Errors[F, E], + ): I[U[F]] = L.forService[U[F]].map { implicit logging => + UL.zipWithK(uf)(Function2K.apply((l, fx) => l.aroundErr(fx))) + } + + def attachErrLogsNamedIn[I[_], E](name: String)(implicit + I: Functor[I], + UL: U[LoggingErrMid[E, *]], + L: Logs[I, F], + U: MonoidalK[U], + F: Monad[F], + FE: Errors[F, E], + ): I[U[F]] = L.byName(name).map { implicit logging => + UL.zipWithK(uf)(Function2K.apply((l, fx) => l.aroundErr(fx))) + } +} diff --git a/logging/structured/src/main/scala/tofu/logging/LoggingMid.scala b/logging/structured/src/main/scala/tofu/logging/LoggingMid.scala new file mode 100644 index 000000000..5c853956a --- /dev/null +++ b/logging/structured/src/main/scala/tofu/logging/LoggingMid.scala @@ -0,0 +1,44 @@ +package tofu.logging + +import cats.Monad +import tofu.Errors +import tofu.higherKind.Mid +import tofu.higherKind.derived.HigherKindedMacros + +/** Logging middleware + * Alg[LoggingMid] is a special form of implicit evidence of injectable logging support + * generally you don't need `Logging` instance to derive this + * so choice of logging postponed until this middleware is attached to the core instance + */ +abstract class LoggingMid[A] { + def around[F[_]: Monad: LoggingBase](fa: F[A]): F[A] + + def toMid[F[_]: Monad: LoggingBase]: Mid[F, A] = around(_) +} + +object LoggingMid extends builder.LoggingMidBuilder.DefaultImpl { + type Result[A] = LoggingMid[A] + def instance[U[_[_]]]: U[LoggingMid] = macro HigherKindedMacros.factorizeThis[U] + + type Of[U[_[_]]] = U[LoggingMid] +} + +/** Logging middleware supporting error reporting + * Alg[LoggingErrMid[E, *]] is a special form of implicit evidence of injectable logging support + * generally you don't need `Logging` instance to derive this + * so choice of logging postponed until this middleware is attached to the core instance + */ +abstract class LoggingErrMid[E, A] extends LoggingMid[A] { + def aroundErr[F[_]: Monad: Errors[*[_], E]: LoggingBase](fa: F[A]): F[A] + + def around[F[_]: Monad: LoggingBase](fa: F[A]): F[A] = around(fa) + + def toMid[F[_]: Monad: Errors[*[_], E]: LoggingBase]: Mid[F, A] = aroundErr(_) +} + +object LoggingErrMid { + type Of[U[_[_]], E] = U[LoggingErrMid[E, *]] + type Try[U[_[_]]] = U[LoggingErrMid[Throwable, *]] + + object Try extends builder.LoggingErrMidBuilder.DefaultImpl[Throwable] +} diff --git a/logging/structured/src/main/scala/tofu/logging/Logs.scala b/logging/structured/src/main/scala/tofu/logging/Logs.scala index 1ac87494f..81b84a264 100644 --- a/logging/structured/src/main/scala/tofu/logging/Logs.scala +++ b/logging/structured/src/main/scala/tofu/logging/Logs.scala @@ -20,6 +20,10 @@ import scala.reflect.ClassTag import tofu.higherKind.Pre import tofu.higherKind.Post import tofu.higherKind.Mid +import tofu.logging.impl.UniversalLogs +import tofu.Delay +import tofu.WithContext +import tofu.logging.impl.UniversalContextLogs /** A helper for creating instances of [[tofu.logging.Logging]], defining a way these instances will behave while doing logging. * Can create instances either on a by-name basic or a type tag basic. @@ -44,7 +48,7 @@ trait Logs[+I[_], F[_]] extends LogsVOps[I, F] { /** Creates an instance of [[tofu.logging.Logging]] with a given arbitrary type tag, * using it as a class to create underlying [[org.slf4j.Logger]] with. */ - def forService[Svc: ClassTag]: I[Logging[F]] + def forService[Svc](implicit Svc: ClassTag[Svc]): I[Logging[F]] = byName(Svc.runtimeClass.getName()) /** Creates an instance of [[tofu.logging.Logging]] for a given arbitrary string name, * using it to create underlying [[org.slf4j.Logger]] with. @@ -60,7 +64,9 @@ trait Logs[+I[_], F[_]] extends LogsVOps[I, F] { } object Logs extends LogsInstances0 { - type Universal[F[_]] = Logs[Id, F] + type Universal[F[_]] = Logs[Id, F] + type Safe[I[_], F[_, _]] = Logs[I, F[Nothing, *]] + type SafeUniversal[F[_, _]] = Logs[Id, F[Nothing, *]] def apply[I[_], F[_]](implicit logs: Logs[I, F]): Logs[I, F] = logs @@ -80,17 +86,19 @@ object Logs extends LogsInstances0 { * Has no notion of context. */ def sync[I[_]: Sync, F[_]: Sync]: Logs[I, F] = new Logs[I, F] { - def forService[Svc: ClassTag]: I[Logging[F]] = - Sync[I].delay(new SyncLogging[F](loggerForService[Svc])) - def byName(name: String): I[Logging[F]] = Sync[I].delay(new SyncLogging[F](LoggerFactory.getLogger(name))) + def byName(name: String): I[Logging[F]] = Sync[I].delay(new SyncLogging[F](LoggerFactory.getLogger(name))) } + def universal[F[_]: Delay]: Logs.Universal[F] = new UniversalLogs + def contextual[F[_]: FlatMap: Delay, C: Loggable](implicit FC: F WithContext C): Logs.Universal[F] = + new UniversalContextLogs[F, C] + def withContext[I[_]: Sync, F[_]: Sync](implicit ctx: LoggableContext[F]): Logs[I, F] = { import ctx.loggable new Logs[I, F] { - def forService[Svc: ClassTag]: I[Logging[F]] = + override def forService[Svc: ClassTag]: I[Logging[F]] = Sync[I].delay(new ContextSyncLoggingImpl[F, ctx.Ctx](ctx.context, loggerForService[Svc])) - override def byName(name: String): I[Logging[F]] = + override def byName(name: String): I[Logging[F]] = Sync[I].delay(new ContextSyncLoggingImpl[F, ctx.Ctx](ctx.context, LoggerFactory.getLogger(name))) } } @@ -98,8 +106,7 @@ object Logs extends LogsInstances0 { /** Returns an instance of [[tofu.logging.Logs]] that will produce the same constant [[tofu.logging.Logging]] instances. */ def const[I[_]: Applicative, F[_]](logging: Logging[F]): Logs[I, F] = new Logs[I, F] { - def forService[Svc: ClassTag]: I[Logging[F]] = logging.pure[I] - def byName(name: String): I[Logging[F]] = logging.pure[I] + def byName(name: String): I[Logging[F]] = logging.pure[I] } /** Returns an instance of [[tofu.logging.Logs]] that will produce a no-op [[tofu.logging.Logging]] instances. @@ -111,8 +118,7 @@ object Logs extends LogsInstances0 { * Resulting [[tofu.logging.Logging]] instance will call both implementations in order. */ def combine[I[_]: Apply, F[_]: Apply](las: Logs[I, F], lbs: Logs[I, F]): Logs[I, F] = new Logs[I, F] { - def forService[Svc: ClassTag]: I[Logging[F]] = las.forService.map2(lbs.forService)(Logging.combine[F]) - def byName(name: String): I[Logging[F]] = las.byName(name).map2(lbs.byName(name))(Logging.combine[F]) + def byName(name: String): I[Logging[F]] = las.byName(name).map2(lbs.byName(name))(Logging.combine[F]) } implicit def logsMonoid[I[_]: Applicative, F[_]: Applicative]: Monoid[Logs[I, F]] = new Monoid[Logs[I, F]] { @@ -191,8 +197,8 @@ trait Logs2FunctorK[Y[_]] extends FunctorK[Logs[Y, *[_]]] { implicit def I: Functor[Y] def mapK[F[_], G[_]](af: Logs[Y, F])(fk: F ~> G): Logs[Y, G] = new Logs[Y, G] { - def forService[Svc: ClassTag]: Y[Logging[G]] = af.forService[Svc].map(_.mapK(fk)) - def byName(name: String): Y[Logging[G]] = af.byName(name).map(_.mapK(fk)) + override def forService[Svc: ClassTag]: Y[Logging[G]] = af.forService[Svc].map(_.mapK(fk)) + def byName(name: String): Y[Logging[G]] = af.byName(name).map(_.mapK(fk)) } } @@ -201,8 +207,9 @@ trait Logs2ApplyK[Y[_]] extends Logs2FunctorK[Y] with ApplyK[Logs[Y, *[_]]] { def zipWith2K[F[_], G[_], H[_]](af: Logs[Y, F], ag: Logs[Y, G])(f2: Function2K[F, G, H]): Logs[Y, H] = new Logs[Y, H] { - def forService[Svc: ClassTag]: Y[Logging[H]] = (af.forService[Svc], ag.forService[Svc]).mapN(_.zipWithK(_)(f2)) - def byName(name: String): Y[Logging[H]] = (af.byName(name), ag.byName(name)).mapN(_.zipWithK(_)(f2)) + override def forService[Svc: ClassTag]: Y[Logging[H]] = + (af.forService[Svc], ag.forService[Svc]).mapN(_.zipWithK(_)(f2)) + def byName(name: String): Y[Logging[H]] = (af.byName(name), ag.byName(name)).mapN(_.zipWithK(_)(f2)) } def productK[F[_], G[_]](af: Logs[Y, F], ag: Logs[Y, G]): Logs[Y, Tuple2K[F, G, *]] = @@ -213,7 +220,6 @@ trait Logs2MonoidalK[Y[_]] extends Logs2ApplyK[Y] with MonoidalK[Logs[Y, *[_]]] implicit def I: Applicative[Y] def pureK[F[_]](p: Point[F]): Logs[Y, F] = new Logs[Y, F] { - def forService[Svc: ClassTag]: Y[Logging[F]] = p.pureK[Logging].pure[Y] - def byName(name: String): Y[Logging[F]] = p.pureK[Logging].pure[Y] + def byName(name: String): Y[Logging[F]] = p.pureK[Logging].pure[Y] } } diff --git a/logging/structured/src/main/scala/tofu/logging/bi/LoggingBiCompanion.scala b/logging/structured/src/main/scala/tofu/logging/bi/LoggingBiCompanion.scala new file mode 100644 index 000000000..c5911a5ef --- /dev/null +++ b/logging/structured/src/main/scala/tofu/logging/bi/LoggingBiCompanion.scala @@ -0,0 +1,85 @@ +package tofu.logging +package bi + +import scala.reflect.ClassTag + +import cats.{Functor, Id} +import tofu.control.Bind +import tofu.higherKind.bi.{BiMid, Fun2BK, FunctorBK, MonoidalBK} +import tofu.logging.ServiceLogging +import tofu.syntax.functorbk._ +import tofu.syntax.monadic._ + +trait LoggingBiCompanion[U[_[_, _]]] { + type Log[F[_, _]] = ServiceLogging[F[Nothing, *], U[Any]] + + implicit def toBiLogBiMidOps[F[+_, +_]](uf: U[F]): LogBiMidOps[U, F] = new LogBiMidOps(uf) + + def bimid[F[+_, +_]: Bind](implicit + logs: Logs.SafeUniversal[F], + UCls: ClassTag[U[F]], + lmid: U[LoggingBiMid], + U: FunctorBK[U], + ): U[BiMid[F, *, *]] = Logging.mid.inBi[U, Id, F] + + def bimidNamed[F[+_, +_]: Bind](name: String)(implicit + logs: Logs.SafeUniversal[F], + lmid: U[LoggingBiMid], + U: FunctorBK[U], + ): U[BiMid[F, *, *]] = Logging.mid.namedBi[U, Id, F](name) + + def bimidIn[I[_]: Functor, F[+_, +_]: Bind](implicit + logs: Logs.Safe[I, F], + UCls: ClassTag[U[F]], + lmid: U[LoggingBiMid], + U: FunctorBK[U], + ): I[U[BiMid[F, *, *]]] = Logging.mid.inBi[U, I, F] + + def bimidNamedIn[I[_]: Functor, F[+_, +_]: Bind](name: String)(implicit + logs: Logs.Safe[I, F], + lmid: U[LoggingBiMid], + U: FunctorBK[U], + ): I[U[BiMid[F, *, *]]] = Logging.mid.namedBi[U, I, F](name) +} + +class LogBiMidOps[U[f[_, _]], F[+_, +_]](private val uf: U[F]) extends AnyVal { + def attachLogs(implicit + logs: Logs.SafeUniversal[F], + UCls: ClassTag[U[F]], + UL: U[LoggingBiMid], + U: MonoidalBK[U], + F: Bind[F], + ): U[F] = { + implicit val logging: Logging.Safe[F] = logs.forService[U[F]] + UL.map2b(uf)(Fun2BK.apply((l, fx) => l.around(fx))) + } + + def attachLogsIn[I[_]: Functor](implicit + logs: Logs.Safe[I, F], + UCls: ClassTag[U[F]], + UL: U[LoggingBiMid], + U: MonoidalBK[U], + F: Bind[F], + ): I[U[F]] = logs.forService[U[F]].map { implicit logging => + UL.map2b(uf)(Fun2BK.apply((l, fx) => l.around(fx))) + } + + def attachLogsNamed(name: String)(implicit + logs: Logs.SafeUniversal[F], + UL: U[LoggingBiMid], + U: MonoidalBK[U], + F: Bind[F], + ): U[F] = { + implicit val logging: Logging.Safe[F] = logs.byName(name) + UL.map2b(uf)(Fun2BK.apply((l, fx) => l.around(fx))) + } + + def attachLogsNamedIn[I[_]: Functor](name: String)(implicit + logs: Logs.Safe[I, F], + UL: U[LoggingBiMid], + U: MonoidalBK[U], + F: Bind[F], + ): I[U[F]] = logs.byName(name).map { implicit logging => + UL.map2b(uf)(Fun2BK.apply((l, fx) => l.around(fx))) + } +} diff --git a/logging/structured/src/main/scala/tofu/logging/bi/LoggingBiMid.scala b/logging/structured/src/main/scala/tofu/logging/bi/LoggingBiMid.scala new file mode 100644 index 000000000..7a907836c --- /dev/null +++ b/logging/structured/src/main/scala/tofu/logging/bi/LoggingBiMid.scala @@ -0,0 +1,20 @@ +package tofu.logging.bi + +import tofu.control.Bind +import tofu.higherKind.bi.BiMid +import tofu.logging.{Logging, builder} + +/** Logging middleware for binary typeconstructor parameterized algebras + * Alg[LoggingBiMid] is a special form of implicit evidence of injectable logging support + * generally you don't need `Logging` instance to derive this + * so choice of logging postponed until this middleware is attached to the core instance + */ +abstract class LoggingBiMid[E, A] { + def around[F[+_, +_]: Bind: Logging.SafeBase](fa: F[E, A]): F[E, A] + + def toMid[F[+_, +_]: Bind: Logging.SafeBase]: BiMid[F, E, A] = fx => around(fx) +} + +object LoggingBiMid extends builder.LoggingBiMidBuilder.Default { + type Of[U[_[_, _]]] = U[LoggingBiMid] +} diff --git a/logging/structured/src/main/scala/tofu/logging/builder/LoggingBiMidBuilder.scala b/logging/structured/src/main/scala/tofu/logging/builder/LoggingBiMidBuilder.scala new file mode 100644 index 000000000..3aa356d06 --- /dev/null +++ b/logging/structured/src/main/scala/tofu/logging/builder/LoggingBiMidBuilder.scala @@ -0,0 +1,94 @@ +package tofu.logging +package builder +import scala.collection.mutable.Buffer +import scala.reflect.ClassTag + +import tofu.control.Bind +import tofu.logging.bi.LoggingBiMid +import tofu.logging.impl.ArgsLoggable +import tofu.logging.{Loggable, LoggedValue, Logging} +import tofu.syntax.bindInv._ + +trait BiBuilder[+T[_, _]] { + def prepare[Alg[_[_, _]]](implicit Alg: ClassTag[Alg[Any]]): BiPrepared[Alg, T] +} + +trait BiMethod[U[f[_, _]], Err, Res, +T[_, _]] { + def arg[A: Loggable](name: String, a: A): BiMethod[U, Err, Res, T] + + def result: T[Err, Res] +} + +trait BiPrepared[U[f[_, _]], +T[_, _]] { + def start[Err: Loggable, Res: Loggable](method: String): BiMethod[U, Err, Res, T] +} + +abstract class LoggingBiMidBuilder extends BiBuilder[LoggingBiMid] { + + /** do some logging upon enter to method invocation */ + def onEnter[F[+_, +_]: Logging.SafeBase]( + cls: Class[_], + method: String, + args: Seq[(String, LoggedValue)] + ): F[Nothing, Unit] + + /** do some logging after leaving method invocation with known result or error */ + def onLeave[F[+_, +_]: Logging.SafeBase]( + cls: Class[_], + method: String, + args: Seq[(String, LoggedValue)], + res: LoggedValue, + ok: Boolean + ): F[Nothing, Unit] + + def prepare[Alg[_[_, _]]](implicit Alg: ClassTag[Alg[Any]]) = new PreparedImpl[Alg](Alg.runtimeClass) + + protected class MethodImpl[U[f[_, _]], Err: Loggable, Res: Loggable]( + cls: Class[_], + method: String, + args: Buffer[(String, LoggedValue)] + ) extends BiMethod[U, Err, Res, LoggingBiMid] { + def arg[A: Loggable](name: String, a: A) = { + args += (name -> a) + this + } + + def result: LoggingBiMid[Err, Res] = new LoggingBiMid[Err, Res] { + private[this] val argSeq = args.toSeq + + def around[F[+_, +_]: Bind: Logging.SafeBase](fa: F[Err, Res]): F[Err, Res] = + onEnter(cls, method, argSeq) *> + fa.tapBoth( + err => onLeave(cls, method, argSeq, err, ok = false), + res => onLeave(cls, method, argSeq, res, ok = true) + ) + } + } + + class PreparedImpl[U[f[_, _]]](cls: Class[_]) extends BiPrepared[U, LoggingBiMid] { + def start[Err: Loggable, Res: Loggable](method: String) = new MethodImpl[U, Err, Res](cls, method, Buffer()) + } +} + +object LoggingBiMidBuilder { + class Default extends LoggingBiMidBuilder { + def onEnter[F[_, _]](cls: Class[_], method: String, args: Seq[(String, LoggedValue)])(implicit + F: Logging.SafeBase[F] + ): F[Nothing, Unit] = F.debug("entering {} {}", cls.getName(), method, new ArgsLoggable(args)) + + def onLeave[F[_, _]]( + cls: Class[_], + method: String, + args: Seq[(String, LoggedValue)], + res: LoggedValue, + ok: Boolean, + )(implicit + F: Logging.SafeBase[F] + ): F[Nothing, Unit] = + if (ok) + F.debug("leaving {} {} result is {}", method, new ArgsLoggable(args), res) + else + F.error("error during {} {} error is {}", method, new ArgsLoggable(args), res) + } + +} diff --git a/logging/structured/src/main/scala/tofu/logging/builder/LoggingMidBuilder.scala b/logging/structured/src/main/scala/tofu/logging/builder/LoggingMidBuilder.scala new file mode 100644 index 000000000..1ce977f29 --- /dev/null +++ b/logging/structured/src/main/scala/tofu/logging/builder/LoggingMidBuilder.scala @@ -0,0 +1,121 @@ +package tofu.logging +package builder + +import scala.collection.mutable +import scala.reflect.ClassTag + +import tofu.syntax.monadic._ +import cats.Monad +import tofu.Errors +import tofu.syntax.handle._ +import impl.ArgsLoggable + +trait Builder[+T[_]] { + def prepare[Alg[_[_]]](implicit Alg: ClassTag[Alg[Any]]): Prepared[Alg, T] +} + +trait Method[U[f[_]], Res, +T[_]] { + def arg[A: Loggable](name: String, a: A): Method[U, Res, T] + def result: T[Res] +} + +trait Prepared[U[f[_]], +T[_]] { + def start[Res: Loggable](method: String): Method[U, Res, T] +} + +/** Logging middleware generator */ +trait LoggingMidBuilder extends Builder[LoggingMid] { + + /** do some logging upon enter to method invocation */ + def onEnter[F[_]: LoggingBase](cls: Class[_], method: String, args: Seq[(String, LoggedValue)]): F[Unit] + + /** do some logging after leaving method invocation with known result */ + def onLeave[F[_]: LoggingBase]( + cls: Class[_], + method: String, + args: Seq[(String, LoggedValue)], + res: LoggedValue + ): F[Unit] + + def prepare[Alg[_[_]]](implicit Alg: ClassTag[Alg[Any]]) = new PreparedImpl[Alg](Alg.runtimeClass) + + protected class MethodImpl[U[f[_]], Res: Loggable]( + cls: Class[_], + method: String, + args: mutable.Buffer[(String, LoggedValue)] + ) extends Method[U, Res, LoggingMid] { + def arg[A: Loggable](name: String, a: A): this.type = { + args += (name -> a) + this + } + def result: LoggingMid[Res] = new LoggingMid[Res] { + private[this] val argSeq = args.toSeq + def around[F[_]: Monad: LoggingBase](fa: F[Res]): F[Res] = + onEnter(cls, method, argSeq) *> fa.flatTap(res => onLeave(cls, method, argSeq, res)) + } + } + + protected class PreparedImpl[U[f[_]]](cls: Class[_]) extends Prepared[U, LoggingMid] { + def start[Res: Loggable](method: String): MethodImpl[U, Res] = new MethodImpl(cls, method, mutable.Buffer()) + } +} + +object LoggingMidBuilder { + trait Default extends LoggingMidBuilder { + def onEnter[F[_]](cls: Class[_], method: String, args: Seq[(String, LoggedValue)])(implicit + F: LoggingBase[F] + ): F[Unit] = F.debug("entering {} {}", method, new ArgsLoggable(args)) + + def onLeave[F[_]](cls: Class[_], method: String, args: Seq[(String, LoggedValue)], res: LoggedValue)(implicit + F: LoggingBase[F] + ): F[Unit] = F.debug("leaving {} {} with result {}", method, new ArgsLoggable(args), res) + } + + class DefaultImpl extends Default +} + +trait LoggingErrMidBuilder[E] extends LoggingMidBuilder with Builder[LoggingErrMid[E, *]] { + def onFault[F[_]: LoggingBase]( + cls: Class[_], + method: String, + args: Seq[(String, LoggedValue)], + err: E + ): F[Unit] + + override def prepare[Alg[_[_]]](implicit Alg: ClassTag[Alg[Any]]) = new PreparedErr[Alg](Alg.runtimeClass) + + protected class MethodErrImpl[U[f[_]], Res: Loggable]( + cls: Class[_], + method: String, + args: mutable.Buffer[(String, LoggedValue)] + ) extends MethodImpl[U, Res](cls, method, args) with Method[U, Res, LoggingErrMid[E, *]] { + override def result: LoggingErrMid[E, Res] = new LoggingErrMid[E, Res] { + private[this] val argSeq = args.toSeq + def aroundErr[F[_]: Monad: Errors[*[_], E]: LoggingBase](fa: F[Res]): F[Res] = + onEnter(cls, method, argSeq) *> + fa.onError(onFault(cls, method, argSeq, _: E)) + .flatTap(res => onLeave(cls, method, argSeq, res)) + } + } + + class PreparedErr[U[f[_]]](cls: Class[_]) extends super.PreparedImpl[U](cls) with Prepared[U, LoggingErrMid[E, *]] { + override def start[Res: Loggable](method: String): MethodErrImpl[U, Res] = + new MethodErrImpl(cls, method, mutable.Buffer()) + } +} + +object LoggingErrMidBuilder { + trait Default[E] extends LoggingMidBuilder.Default with LoggingErrMidBuilder[E] { + implicit def errLoggable: Loggable[E] + + def onFault[F[_]]( + cls: Class[_], + method: String, + args: Seq[(String, LoggedValue)], + err: E + )(implicit F: LoggingBase[F]): F[Unit] = + F.error("error during {} {} error is {}", method, new ArgsLoggable(args), err) + + } + class DefaultImpl[E](implicit val errLoggable: Loggable[E]) extends Default[E] +} diff --git a/logging/structured/src/main/scala/tofu/logging/impl/ArgsLoggable.scala b/logging/structured/src/main/scala/tofu/logging/impl/ArgsLoggable.scala new file mode 100644 index 000000000..c368eb656 --- /dev/null +++ b/logging/structured/src/main/scala/tofu/logging/impl/ArgsLoggable.scala @@ -0,0 +1,13 @@ +package tofu.logging.impl + +import tofu.logging.{LogRenderer, LoggedValue} + +class ArgsLoggable(values: Seq[(String, LoggedValue)]) extends LoggedValue { + override def shortName: String = "arguments" + + override def toString = values.map { case (name, value) => s"$name = $value" }.mkString("(", ", ", ")") + + def logFields[I, V, @specialized R, @specialized M](input: I)(implicit r: LogRenderer[I, V, R, M]): R = { + values.foldLeft(r.noop(input)) { (res, p) => r.combine(res, r.field(p._1, input, p._2)) } + } +} diff --git a/logging/structured/src/main/scala/tofu/logging/impl/CachedLogs.scala b/logging/structured/src/main/scala/tofu/logging/impl/CachedLogs.scala index 82a27a037..31cba852a 100644 --- a/logging/structured/src/main/scala/tofu/logging/impl/CachedLogs.scala +++ b/logging/structured/src/main/scala/tofu/logging/impl/CachedLogs.scala @@ -30,6 +30,6 @@ class CachedLogs[I[_]: Monad: Guarantee, F[_]]( case logging => logging.pure[I] }) - def forService[Svc: ClassTag]: I[Logging[F]] = safeGet(tagCache, underlying.forService[Svc], classTag[Svc]) - def byName(name: String): I[Logging[F]] = safeGet(nameCache, underlying.byName(name), name) + override def forService[Svc: ClassTag]: I[Logging[F]] = safeGet(tagCache, underlying.forService[Svc], classTag[Svc]) + def byName(name: String): I[Logging[F]] = safeGet(nameCache, underlying.byName(name), name) } diff --git a/logging/structured/src/main/scala/tofu/logging/impl/LoggingMidFunctions.scala b/logging/structured/src/main/scala/tofu/logging/impl/LoggingMidFunctions.scala new file mode 100644 index 000000000..562d4a08b --- /dev/null +++ b/logging/structured/src/main/scala/tofu/logging/impl/LoggingMidFunctions.scala @@ -0,0 +1,54 @@ +package tofu.logging + +import scala.reflect.ClassTag + +import cats.tagless.FunctorK +import cats.{Functor, Monad} +import tofu.higherKind.Mid +import cats.tagless.syntax.functorK._ +import tofu.syntax.functorbk._ +import tofu.syntax.monadic._ +import tofu.syntax.funk._ +import tofu.higherKind.bi.FunctorBK +import tofu.control.Bind +import tofu.logging.bi.LoggingBiMid +import tofu.higherKind.bi.BiMid +import tofu.higherKind.bi.FunBK +import tofu.Errors + +object LoggingMidFunctions { + def in[U[_[_]]: FunctorK, I[_]: Functor, F[_]: Monad](implicit + logs: Logs[I, F], + UCls: ClassTag[U[F]], + lmid: U[LoggingMid], + ): I[U[Mid[F, *]]] = logs.forService[U[F]].map(implicit logging => lmid.mapK(funK(_.toMid))) + + def named[U[_[_]]: FunctorK, I[_]: Functor, F[_]: Monad](name: String)(implicit + logs: Logs[I, F], + lmid: U[LoggingMid], + ): I[U[Mid[F, *]]] = logs.byName(name).map(implicit logging => lmid.mapK(funK(_.toMid))) + + def inBi[U[_[_, _]]: FunctorBK, I[_]: Functor, F[+_, +_]: Bind](implicit + logs: Logs.Safe[I, F], + UCls: ClassTag[U[F]], + lmid: U[LoggingBiMid], + ): I[U[BiMid[F, *, *]]] = logs.forService[U[F]].map(implicit logging => lmid.mapb(FunBK.apply(_.toMid))) + + def namedBi[U[_[_, _]]: FunctorBK, I[_]: Functor, F[+_, +_]: Bind](name: String)(implicit + logs: Logs.Safe[I, F], + lmid: U[LoggingBiMid], + ): I[U[BiMid[F, *, *]]] = logs.byName(name).map(implicit logging => lmid.mapb(FunBK.apply(_.toMid))) + + def errIn[U[_[_]]: FunctorK, I[_]: Functor, F[+_]: Monad, E](implicit + logs: Logs[I, F], + UCls: ClassTag[U[F]], + lmid: LoggingErrMid.Of[U, E], + errs: Errors[F, E], + ): I[U[Mid[F, *]]] = logs.forService[U[F]].map(implicit logging => lmid.mapK(funK(_.toMid[F]))) + + def namedErr[U[_[_]]: FunctorK, I[_]: Functor, F[+_]: Monad, E](name: String)(implicit + logs: Logs[I, F], + lmid: LoggingErrMid.Of[U, E], + errs: Errors[F, E], + ): I[U[Mid[F, *]]] = logs.byName(name).map(implicit logging => lmid.mapK(funK(_.toMid[F]))) +} diff --git a/logging/structured/src/main/scala/tofu/logging/impl/UniversalEmbedLogs.scala b/logging/structured/src/main/scala/tofu/logging/impl/UniversalEmbedLogs.scala index 074b6449a..74ffbb46a 100644 --- a/logging/structured/src/main/scala/tofu/logging/impl/UniversalEmbedLogs.scala +++ b/logging/structured/src/main/scala/tofu/logging/impl/UniversalEmbedLogs.scala @@ -7,8 +7,8 @@ import scala.reflect.ClassTag class UniversalEmbedLogs[I[_], F[_]: FlatMap](underlying: Logs[I, F])(implicit lift: Lift[I, F]) extends Logs.Universal[F] { - def forService[Svc: ClassTag]: Logging[F] = + override def forService[Svc: ClassTag]: Logging[F] = Logging.loggingRepresentable.embed(lift.lift(underlying.forService[Svc])) - def byName(name: String): Id[Logging[F]] = + def byName(name: String): Id[Logging[F]] = Logging.loggingRepresentable.embed(lift.lift(underlying.byName(name))) } diff --git a/logging/structured/src/main/scala/tofu/logging/impl/UniversalLogging.scala b/logging/structured/src/main/scala/tofu/logging/impl/UniversalLogging.scala new file mode 100644 index 000000000..79d2c250f --- /dev/null +++ b/logging/structured/src/main/scala/tofu/logging/impl/UniversalLogging.scala @@ -0,0 +1,83 @@ +package tofu.logging +package impl + +import cats.FlatMap +import org.slf4j.{Logger, LoggerFactory, Marker} +import tofu.logging.Logging.{Debug, Error, Info, Trace, Warn} +import tofu.{Delay, WithContext} + +object UniversalLogging { + private[impl] final def enabled(level: Logging.Level, logger: Logger): Boolean = level match { + case Trace => logger.isTraceEnabled() + case Debug => logger.isDebugEnabled() + case Info => logger.isInfoEnabled() + case Warn => logger.isWarnEnabled() + case Error => logger.isErrorEnabled() + } + + private[impl] final def write(level: Logging.Level, logger: Logger, message: String, values: Seq[LoggedValue]): Unit = + level match { + case Trace => logger.trace(message, values: _*) + case Debug => logger.debug(message, values: _*) + case Info => logger.info(message, values: _*) + case Warn => logger.warn(message, values: _*) + case Error => logger.error(message, values: _*) + } + + private[impl] final def writeMarker( + level: Logging.Level, + logger: Logger, + marker: Marker, + message: String, + values: Seq[LoggedValue] + ): Unit = + level match { + case Trace => logger.trace(marker, message, values: _*) + case Debug => logger.debug(marker, message, values: _*) + case Info => logger.info(marker, message, values: _*) + case Warn => logger.warn(marker, message, values: _*) + case Error => logger.error(marker, message, values: _*) + } +} + +class UniversalLogging[F[_]](name: String)(implicit F: Delay[F]) extends Logging[F] { + def write(level: Logging.Level, message: String, values: LoggedValue*): F[Unit] = + F.delay { + val logger = LoggerFactory.getLogger(name) + if (UniversalLogging.enabled(level, logger)) + UniversalLogging.write(level, logger, message, values) + } + + override def writeMarker(level: Logging.Level, message: String, marker: Marker, values: LoggedValue*): F[Unit] = + F.delay { + val logger = LoggerFactory.getLogger(name) + if (UniversalLogging.enabled(level, logger)) + UniversalLogging.writeMarker(level, logger, marker, message, values) + } +} + +class UniversalContextLogging[F[_]](name: String, fctx: (LoggedValue => Unit) => F[Unit]) extends Logging[F] { + def write(level: Logging.Level, message: String, values: LoggedValue*): F[Unit] = + fctx { ctx => + val logger = LoggerFactory.getLogger(name) + if (UniversalLogging.enabled(level, logger)) + UniversalLogging.writeMarker(level, logger, ContextMarker(ctx), message, values) + } + + override def writeMarker(level: Logging.Level, message: String, marker: Marker, values: LoggedValue*): F[Unit] = + fctx { ctx => + val logger = LoggerFactory.getLogger(name) + if (UniversalLogging.enabled(level, logger)) + UniversalLogging.writeMarker(level, logger, ContextMarker(ctx, List(marker)), message, values) + } +} + +class UniversalLogs[F[_]: Delay] extends Logs.Universal[F] { + override def byName(name: String): Logging[F] = new UniversalLogging[F](name) +} + +class UniversalContextLogs[F[_]: FlatMap, C: Loggable](implicit FC: F WithContext C, FD: Delay[F]) + extends Logs.Universal[F] { + private def useContextValue(f: LoggedValue => Unit): F[Unit] = FC.askF(c => FD.delay(f(c))) + override def byName(name: String): Logging[F] = new UniversalContextLogging[F](name, useContextValue) +} diff --git a/logging/structured/src/test/scala/tofu/logging/LoggingSuite.scala b/logging/structured/src/test/scala/tofu/logging/LoggingSuite.scala index 6fd36e825..edf973b43 100644 --- a/logging/structured/src/test/scala/tofu/logging/LoggingSuite.scala +++ b/logging/structured/src/test/scala/tofu/logging/LoggingSuite.scala @@ -9,8 +9,6 @@ import tofu.syntax.logRenderer._ import tofu.syntax.loggable._ import tofu.syntax.logging._ -import scala.reflect.{ClassTag, classTag} - class LoggingSuite extends AnyFlatSpec { val exprs = new Exprs[Run] @@ -79,8 +77,6 @@ object LoggingSuite { type Run[+A] = ICalc[Pasque, Vector[LogEntry], Nothing, A] implicit val logs: Logs.Universal[Run] = new Logs.Universal[Run] { - def forService[Svc: ClassTag]: Logging[Run] = byName(classTag[Svc].runtimeClass.getName) - def byName(name: String): Logging[Run] = (level, message, values) => (Calc.read: Run[Pasque]).flatMap { ctx => diff --git a/logging/util/src/main/scala/tofu/logging/atom/AtomLogger.scala b/logging/util/src/main/scala/tofu/logging/atom/AtomLogger.scala index 57133a6d2..775d8de7d 100644 --- a/logging/util/src/main/scala/tofu/logging/atom/AtomLogger.scala +++ b/logging/util/src/main/scala/tofu/logging/atom/AtomLogger.scala @@ -37,7 +37,7 @@ class AtomLogging[F[_]: FlatMap: Clock](log: Atom[F, Vector[LogLine]], name: Str final case class AtomLogs[I[_]: Applicative, F[_]: FlatMap: Clock](flog: F[Atom[F, Vector[LogLine]]]) extends Logs[I, F] { - def forService[Svc: ClassTag]: I[Logging[F]] = byName(classTag[Svc].runtimeClass.getName) - def byName(name: String): I[Logging[F]] = + override def forService[Svc: ClassTag]: I[Logging[F]] = byName(classTag[Svc].runtimeClass.getName) + def byName(name: String): I[Logging[F]] = Embed.of(flog.map[Logging[F]](new AtomLogging[F](_, name))).pure[I] } diff --git a/optics/core/src/main/scala/tofu/optics/Downcast.scala b/optics/core/src/main/scala/tofu/optics/Downcast.scala index 772957250..3bc5acecc 100644 --- a/optics/core/src/main/scala/tofu/optics/Downcast.scala +++ b/optics/core/src/main/scala/tofu/optics/Downcast.scala @@ -14,6 +14,8 @@ import scala.reflect.{ClassTag, classTag} trait PDowncast[-S, +T, +A, -B] extends PFolded[S, T, A, B] with PBase[PDowncast, S, T, A, B] { def downcast(s: S): Option[A] + override def as[B1, T1]: PDowncast[S, T1, A, B1] = this.asInstanceOf[PDowncast[S, T1, A, B1]] + def getOption(s: S): Option[A] = downcast(s) def foldMap[X: Monoid](s: S)(f: A => X): X = downcast(s).foldMap(f) diff --git a/optics/core/src/main/scala/tofu/optics/Extract.scala b/optics/core/src/main/scala/tofu/optics/Extract.scala index f15184865..34695186e 100644 --- a/optics/core/src/main/scala/tofu/optics/Extract.scala +++ b/optics/core/src/main/scala/tofu/optics/Extract.scala @@ -12,6 +12,8 @@ trait PExtract[-S, +T, +A, -B] extends PDowncast[S, T, A, B] with PReduced[S, T, A, B] with PBase[PExtract, S, T, A, B] { def extract(s: S): A + override def as[B1, T1]: PExtract[S, T1, A, B1] = this.asInstanceOf[PExtract[S, T1, A, B1]] + def downcast(s: S): Option[A] = Some(extract(s)) def reduceMap[X: Semigroup](s: S)(f: A => X): X = f(extract(s)) override def foldMap[X: Monoid](s: S)(f: A => X): X = f(extract(s)) diff --git a/zio/core/src/main/scala/tofu/zioInstances/ZioTofuInstance.scala b/zio/core/src/main/scala/tofu/zioInstances/ZioTofuInstance.scala index 33baa7bf5..2a2fcf888 100644 --- a/zio/core/src/main/scala/tofu/zioInstances/ZioTofuInstance.scala +++ b/zio/core/src/main/scala/tofu/zioInstances/ZioTofuInstance.scala @@ -23,7 +23,7 @@ import zio.blocking._ class ZioTofuInstance[R, E] extends Errors[ZIO[R, E, *], E] with Start[ZIO[R, E, *]] with Finally[ZIO[R, E, *], Exit[E, *]] - with Execute[ZIO[R, E, *]] { + with Execute[ZIO[R, E, *]] with Delay[ZIO[R, E, *]] { final def restore[A](fa: ZIO[R, E, A]): ZIO[R, E, Option[A]] = fa.option final def raise[A](err: E): ZIO[R, E, A] = ZIO.fail(err) final def lift[A](fa: ZIO[R, E, A]): ZIO[R, E, A] = fa @@ -63,10 +63,14 @@ class ZioTofuInstance[R, E] def executionContext: ZIO[R, E, ExecutionContext] = ZIO.runtime[R].map(_.platform.executor.asEC) def deferFutureAction[A](f: ExecutionContext => Future[A]): ZIO[R, E, A] = ZIO.fromFuture(f).orDie + + def delay[A](a: => A): ZIO[R, E, A] = ZIO.effectTotal(a) } class RioTofuInstance[R] extends ZioTofuInstance[R, Throwable] { override def deferFutureAction[A](f: ExecutionContext => Future[A]): ZIO[R, Throwable, A] = ZIO.fromFuture(f) + + override def delay[A](a: => A): ZIO[R, Throwable, A] = ZIO.effect(a) } class ZioTofuWithRunInstance[R, E] extends WithRun[ZIO[R, E, *], ZIO[Any, E, *], R] { diff --git a/zio/logging/src/main/scala/tofu/logging/zlogs/ZLogs.scala b/zio/logging/src/main/scala/tofu/logging/zlogs/ZLogs.scala index fc7862cfb..16fd560e8 100644 --- a/zio/logging/src/main/scala/tofu/logging/zlogs/ZLogs.scala +++ b/zio/logging/src/main/scala/tofu/logging/zlogs/ZLogs.scala @@ -1,26 +1,21 @@ package tofu.logging.zlogs +import scala.reflect.ClassTag + import izumi.reflect.Tag import org.slf4j.LoggerFactory -import tofu.logging.Logging.loggerForService import tofu.logging.zlogs.impl.{UIOZLogging, URIOZLoggingImpl} import tofu.logging.{Loggable, Logging} import zio.interop.catz._ import zio.{Has, UIO, ULayer, ZIO, ZLayer} - import scala.annotation.nowarn -import scala.reflect.ClassTag object ZLogs { val uio: ZLogs[Any] = new ZLogs[Any] { - def forService[Svc: ClassTag]: UIO[Logging[UIO]] = - UIO.effectTotal(new UIOZLogging(loggerForService[Svc])) - def byName(name: String): UIO[Logging[UIO]] = UIO.effectTotal(new UIOZLogging(LoggerFactory.getLogger(name))) + def byName(name: String): UIO[Logging[UIO]] = UIO.effectTotal(new UIOZLogging(LoggerFactory.getLogger(name))) } def withContext[R: Loggable]: ZLogs[R] = new ZLogs[R] { - def forService[Svc: ClassTag]: UIO[ZLogging[R]] = - UIO.effectTotal(new URIOZLoggingImpl(loggerForService[Svc])) override def byName(name: String): UIO[ZLogging[R]] = UIO.effectTotal(new URIOZLoggingImpl[R](LoggerFactory.getLogger(name))) }