diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/model/Producer.scala b/distage/distage-core-api/src/main/scala/izumi/distage/model/Producer.scala index 246331f71e..3b4b52ed99 100644 --- a/distage/distage-core-api/src/main/scala/izumi/distage/model/Producer.scala +++ b/distage/distage-core-api/src/main/scala/izumi/distage/model/Producer.scala @@ -12,22 +12,22 @@ import izumi.reflect.TagK * @throws izumi.distage.model.exceptions.runtime.ProvisioningException produce* methods raise this exception in `F` effect type on failure */ trait Producer { - private[distage] def produceDetailedFX[F[_]: TagK: QuasiIO](plan: Plan, filter: FinalizerFilter[F]): Lifecycle[F, Either[FailedProvision[F], Locator]] + private[distage] def produceDetailedFX[F[_]: TagK: QuasiIO](plan: Plan, filter: FinalizerFilter[F]): Lifecycle[F, Either[FailedProvision, Locator]] private[distage] final def produceFX[F[_]: TagK: QuasiIO](plan: Plan, filter: FinalizerFilter[F]): Lifecycle[F, Locator] = { - produceDetailedFX[F](plan, filter).evalMap(_.throwOnFailure()) + produceDetailedFX[F](plan, filter).evalMap(_.failOnFailure()) } /** Produce [[izumi.distage.model.Locator]] interpreting effect- and resource-bindings into the provided `F` */ final def produceCustomF[F[_]: TagK: QuasiIO](plan: Plan): Lifecycle[F, Locator] = { produceFX[F](plan, FinalizerFilter.all[F]) } - final def produceDetailedCustomF[F[_]: TagK: QuasiIO](plan: Plan): Lifecycle[F, Either[FailedProvision[F], Locator]] = { + final def produceDetailedCustomF[F[_]: TagK: QuasiIO](plan: Plan): Lifecycle[F, Either[FailedProvision, Locator]] = { produceDetailedFX[F](plan, FinalizerFilter.all[F]) } /** Produce [[izumi.distage.model.Locator]], supporting only effect- and resource-bindings in `Identity` */ final def produceCustomIdentity(plan: Plan): Lifecycle[Identity, Locator] = produceCustomF[Identity](plan) - final def produceDetailedIdentity(plan: Plan): Lifecycle[Identity, Either[FailedProvision[Identity], Locator]] = + final def produceDetailedIdentity(plan: Plan): Lifecycle[Identity, Either[FailedProvision, Locator]] = produceDetailedCustomF[Identity](plan) } diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/model/provisioning/PlanInterpreter.scala b/distage/distage-core-api/src/main/scala/izumi/distage/model/provisioning/PlanInterpreter.scala index 0afbe9b2a2..d8a2e3b9e1 100644 --- a/distage/distage-core-api/src/main/scala/izumi/distage/model/provisioning/PlanInterpreter.scala +++ b/distage/distage-core-api/src/main/scala/izumi/distage/model/provisioning/PlanInterpreter.scala @@ -3,15 +3,15 @@ package izumi.distage.model.provisioning import izumi.distage.model.Locator import izumi.distage.model.definition.Lifecycle import izumi.distage.model.definition.errors.DIError -import izumi.functional.quasi.QuasiIO -import izumi.distage.model.exceptions.* import izumi.distage.model.definition.errors.ProvisionerIssue.* import izumi.distage.model.definition.errors.ProvisionerIssue.ProvisionerExceptionIssue.* +import izumi.distage.model.exceptions.* import izumi.distage.model.exceptions.runtime.{MissingInstanceException, ProvisioningException} import izumi.distage.model.plan.Plan import izumi.distage.model.provisioning.PlanInterpreter.{FailedProvision, FinalizerFilter} -import izumi.distage.model.provisioning.Provision.ProvisionImmutable +import izumi.distage.model.provisioning.Provision.{ProvisionImmutable, ProvisionInstances} import izumi.distage.model.reflection.* +import izumi.functional.quasi.QuasiIO import izumi.fundamentals.platform.IzumiProject import izumi.fundamentals.platform.build.MacroParameters import izumi.fundamentals.platform.exceptions.IzThrowable.* @@ -23,7 +23,7 @@ trait PlanInterpreter { plan: Plan, parentLocator: Locator, filterFinalizers: FinalizerFilter[F], - ): Lifecycle[F, Either[FailedProvision[F], Locator]] + ): Lifecycle[F, Either[FailedProvision, Locator]] } object PlanInterpreter { @@ -41,18 +41,19 @@ object PlanInterpreter { } } - case class FailedProvisionMeta(status: Map[DIKey, OpStatus]) + final case class FailedProvisionMeta(status: Map[DIKey, OpStatus]) + + final case class FailedProvisionInternal[F[_]](provision: ProvisionImmutable[F], fail: FailedProvision) - final case class FailedProvision[F[_]]( - failed: ProvisionImmutable[F], + final case class FailedProvision( + failed: ProvisionInstances, plan: Plan, parentContext: Locator, failure: ProvisioningFailure, meta: FailedProvisionMeta, fullStackTraces: Boolean, ) { - /** @throws ProvisioningException in `F` effect type */ - def throwException[A]()(implicit F: QuasiIO[F]): F[A] = { + def toThrowable(): Throwable = { import FailedProvision.ProvisioningFailureOps val repr = failure.render(fullStackTraces) val ccFailed = failure.status.collect { case (key, _: OpStatus.Failure) => key }.toSet.size @@ -69,21 +70,28 @@ object PlanInterpreter { case _: ProvisioningFailure.CantBuildIntegrationSubplan => Seq.empty } - F.fail { - new ProvisioningException( - s"""Interpreter stopped; out of $ccTotal operations: $ccFailed failed, $ccDone succeeded, $ccPending ignored - |$repr - |""".stripMargin, - null, - ).addAllSuppressed(allExceptions) - } + new ProvisioningException( + s"""Interpreter stopped; out of $ccTotal operations: $ccFailed failed, $ccDone succeeded, $ccPending ignored + |$repr + |""".stripMargin, + null, + ).addAllSuppressed(allExceptions) } + + /** @throws ProvisioningException in `F` effect type */ + } object FailedProvision { - implicit final class FailedProvisionExt[F[_]](private val p: Either[FailedProvision[F], Locator]) extends AnyVal { + implicit final class FailedProvisionExt[F[_]](private val p: Either[FailedProvision, Locator]) extends AnyVal { /** @throws ProvisioningException in `F` effect type */ - def throwOnFailure()(implicit F: QuasiIO[F]): F[Locator] = p.fold(_.throwException(), F.pure) + def failOnFailure()(implicit F: QuasiIO[F]): F[Locator] = p.fold(f => F.fail(f.toThrowable()), F.pure) + def throwOnFailure(): Locator = p match { + case Left(f) => + throw f.toThrowable() + case Right(value) => + value + } } implicit class ProvisioningFailureOps(private val failure: ProvisioningFailure) extends AnyVal { diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/model/provisioning/Provision.scala b/distage/distage-core-api/src/main/scala/izumi/distage/model/provisioning/Provision.scala index a81fe31ea9..ea2c486851 100644 --- a/distage/distage-core-api/src/main/scala/izumi/distage/model/provisioning/Provision.scala +++ b/distage/distage-core-api/src/main/scala/izumi/distage/model/provisioning/Provision.scala @@ -35,12 +35,18 @@ trait Provision[+F[_]] { } object Provision { + final case class ProvisionInstances( + instances: Map[DIKey, Any], + imports: Map[DIKey, Any], + ) + final case class ProvisionImmutable[+F[_]]( // LinkedHashMap for ordering instancesImpl: mutable.LinkedHashMap[DIKey, Any], imports: Map[DIKey, Any], finalizers: Seq[Finalizer[F]], ) extends Provision[F] { + def raw: ProvisionInstances = ProvisionInstances(instances, imports) override def instances: Map[DIKey, Any] = instancesImpl override lazy val enumerate: immutable.Seq[IdentifiedRef] = super.enumerate override lazy val index: immutable.Map[DIKey, Any] = super.index diff --git a/distage/distage-core/src/main/scala/izumi/distage/InjectorDefaultImpl.scala b/distage/distage-core/src/main/scala/izumi/distage/InjectorDefaultImpl.scala index 6c693ebbe5..3c56befc22 100644 --- a/distage/distage-core/src/main/scala/izumi/distage/InjectorDefaultImpl.scala +++ b/distage/distage-core/src/main/scala/izumi/distage/InjectorDefaultImpl.scala @@ -48,7 +48,7 @@ final class InjectorDefaultImpl[F[_]]( override private[distage] def produceDetailedFX[G[_]: TagK: QuasiIO]( plan: Plan, filter: FinalizerFilter[G], - ): Lifecycle[G, Either[FailedProvision[G], Locator]] = { + ): Lifecycle[G, Either[FailedProvision, Locator]] = { interpreter.run[G](plan, bootstrapLocator, filter) } diff --git a/distage/distage-core/src/main/scala/izumi/distage/model/Injector.scala b/distage/distage-core/src/main/scala/izumi/distage/model/Injector.scala index 9e52df7e53..7c80dcf5e7 100644 --- a/distage/distage-core/src/main/scala/izumi/distage/model/Injector.scala +++ b/distage/distage-core/src/main/scala/izumi/distage/model/Injector.scala @@ -230,7 +230,7 @@ trait Injector[F[_]] extends Planner with Producer { .liftF(G.maybeSuspendEither(plan(input).aggregateErrors)) .flatMap(produceCustomF[G]) } - final def produceDetailedCustomF[G[_]: TagK](input: PlannerInput)(implicit G: QuasiIO[G]): Lifecycle[G, Either[FailedProvision[G], Locator]] = { + final def produceDetailedCustomF[G[_]: TagK](input: PlannerInput)(implicit G: QuasiIO[G]): Lifecycle[G, Either[FailedProvision, Locator]] = { Lifecycle .liftF(G.maybeSuspendEither(plan(input).aggregateErrors)) .flatMap(produceDetailedCustomF[G]) @@ -240,7 +240,7 @@ trait Injector[F[_]] extends Planner with Producer { final def produceCustomIdentity(input: PlannerInput): Lifecycle[Identity, Locator] = { produceCustomF[Identity](input) } - final def produceDetailedIdentity(input: PlannerInput): Lifecycle[Identity, Either[FailedProvision[Identity], Locator]] = { + final def produceDetailedIdentity(input: PlannerInput): Lifecycle[Identity, Either[FailedProvision, Locator]] = { produceDetailedCustomF[Identity](input) } diff --git a/distage/distage-core/src/main/scala/izumi/distage/provisioning/PlanInterpreterNonSequentialRuntimeImpl.scala b/distage/distage-core/src/main/scala/izumi/distage/provisioning/PlanInterpreterNonSequentialRuntimeImpl.scala index 1e8acad395..cae0a6cd36 100644 --- a/distage/distage-core/src/main/scala/izumi/distage/provisioning/PlanInterpreterNonSequentialRuntimeImpl.scala +++ b/distage/distage-core/src/main/scala/izumi/distage/provisioning/PlanInterpreterNonSequentialRuntimeImpl.scala @@ -10,7 +10,7 @@ import izumi.distage.model.exceptions.runtime.IntegrationCheckException import izumi.distage.model.plan.ExecutableOp.* import izumi.distage.model.plan.{ExecutableOp, Plan} import izumi.distage.model.provisioning.* -import izumi.distage.model.provisioning.PlanInterpreter.{FailedProvision, FinalizerFilter} +import izumi.distage.model.provisioning.PlanInterpreter.{FailedProvision, FailedProvisionInternal, FinalizerFilter} import izumi.distage.model.provisioning.strategies.* import izumi.distage.model.{Locator, Planner} import izumi.distage.provisioning.PlanInterpreterNonSequentialRuntimeImpl.{abstractCheckType, integrationCheckIdentityType, nullType} @@ -39,32 +39,33 @@ class PlanInterpreterNonSequentialRuntimeImpl( parentLocator: Locator, filterFinalizers: FinalizerFilter[F], )(implicit F: QuasiIO[F] - ): Lifecycle[F, Either[FailedProvision[F], Locator]] = { - Lifecycle.make( - acquire = instantiateImpl(plan, parentLocator) - )(release = { - resource => - val finalizers = resource match { - case Left(failedProvision) => failedProvision.failed.finalizers - case Right(locator) => locator.finalizers - } - filterFinalizers.filter(finalizers).foldLeft(F.unit) { - case (acc, f) => acc.guarantee(F.suspendF(f.effect())) - } - }) + ): Lifecycle[F, Either[FailedProvision, Locator]] = { + Lifecycle + .make( + acquire = instantiateImpl(plan, parentLocator) + )(release = { + resource => + val finalizers = resource match { + case Left(failedProvision) => failedProvision.provision.finalizers + case Right(locator) => locator.finalizers + } + filterFinalizers.filter(finalizers).foldLeft(F.unit) { + case (acc, f) => acc.guarantee(F.suspendF(f.effect())) + } + }).map(_.left.map(_.fail)) } private[this] def instantiateImpl[F[_]: TagK]( plan: Plan, parentContext: Locator, )(implicit F: QuasiIO[F] - ): F[Either[FailedProvision[F], LocatorDefaultImpl[F]]] = { + ): F[Either[FailedProvisionInternal[F], LocatorDefaultImpl[F]]] = { val integrationCheckFType = SafeType.get[IntegrationCheck[F]] val ctx: ProvisionMutable[F] = new ProvisionMutable[F](plan, parentContext) @nowarn("msg=Unused import") - def run(state: TraversalState, integrationPaths: Set[DIKey]): F[Either[TraversalState, Either[FailedProvision[F], LocatorDefaultImpl[F]]]] = { + def run(state: TraversalState, integrationPaths: Set[DIKey]): F[Either[TraversalState, Either[FailedProvisionInternal[F], LocatorDefaultImpl[F]]]] = { import scala.collection.compat.* state.current match { @@ -122,7 +123,7 @@ class PlanInterpreterNonSequentialRuntimeImpl( initial: TraversalState, issues: Iterable[ProvisionerIssue], )(implicit F: QuasiIO[F] - ): F[Either[FailedProvision[F], A]] = { + ): F[Either[FailedProvisionInternal[F], A]] = { val failures = issues.map { issue => TimedFinalResult.Failure( @@ -139,7 +140,7 @@ class PlanInterpreterNonSequentialRuntimeImpl( state: TraversalState, ctx: ProvisionMutable[F], )(implicit F: QuasiIO[F] - ): F[Either[FailedProvision[F], Plan]] = { + ): F[Either[FailedProvisionInternal[F], Plan]] = { val allChecks = ctx.plan.stepsUnordered.iterator.collect { case op: InstantiationOp if op.instanceType <:< abstractCheckType => op }.toSet diff --git a/distage/distage-core/src/main/scala/izumi/distage/provisioning/ProvisionMutable.scala b/distage/distage-core/src/main/scala/izumi/distage/provisioning/ProvisionMutable.scala index 41cd665080..9534339b36 100644 --- a/distage/distage-core/src/main/scala/izumi/distage/provisioning/ProvisionMutable.scala +++ b/distage/distage-core/src/main/scala/izumi/distage/provisioning/ProvisionMutable.scala @@ -5,7 +5,7 @@ import izumi.distage.model.Locator import izumi.distage.model.Locator.LocatorMeta import izumi.distage.model.definition.errors.ProvisionerIssue import izumi.distage.model.plan.Plan -import izumi.distage.model.provisioning.PlanInterpreter.{FailedProvision, FailedProvisionMeta, Finalizer} +import izumi.distage.model.provisioning.PlanInterpreter.{FailedProvision, FailedProvisionInternal, FailedProvisionMeta, Finalizer} import izumi.distage.model.provisioning.Provision.ProvisionImmutable import izumi.distage.model.provisioning.{NewObjectOp, Provision, ProvisioningFailure} import izumi.distage.model.recursive.LocatorRef @@ -28,7 +28,7 @@ final class ProvisionMutable[F[_]: TagK]( Seq(NewObjectOp.NewImport(DIKey.get[LocatorRef], locatorRef)) } - def makeFailure(state: TraversalState, fullStackTraces: Boolean): FailedProvision[F] = { + def makeFailure(state: TraversalState, fullStackTraces: Boolean): FailedProvisionInternal[F] = { val diag = if (state.failures.isEmpty) { ProvisioningFailure.BrokenGraph(state.preds, state.status()) } else { @@ -37,16 +37,20 @@ final class ProvisionMutable[F[_]: TagK]( makeFailure(state, fullStackTraces, diag) } - def makeFailure(state: TraversalState, fullStackTraces: Boolean, diag: ProvisioningFailure): FailedProvision[F] = { + def makeFailure(state: TraversalState, fullStackTraces: Boolean, diag: ProvisioningFailure): FailedProvisionInternal[F] = { val meta = FailedProvisionMeta(state.status()) - FailedProvision( - failed = toImmutable, - plan = plan, - parentContext = parentContext, - failure = diag, - meta = meta, - fullStackTraces = fullStackTraces, + val prov = toImmutable + FailedProvisionInternal( + prov, + FailedProvision( + failed = toImmutable.raw, + plan = plan, + parentContext = parentContext, + failure = diag, + meta = meta, + fullStackTraces = fullStackTraces, + ), ) } diff --git a/distage/distage-core/src/test/scala/izumi/distage/injector/ResourceEffectBindingsTest.scala b/distage/distage-core/src/test/scala/izumi/distage/injector/ResourceEffectBindingsTest.scala index 30f1e36d48..557993114c 100644 --- a/distage/distage-core/src/test/scala/izumi/distage/injector/ResourceEffectBindingsTest.scala +++ b/distage/distage-core/src/test/scala/izumi/distage/injector/ResourceEffectBindingsTest.scala @@ -6,7 +6,6 @@ import izumi.distage.fixtures.ResourceCases.* import izumi.distage.injector.ResourceEffectBindingsTest.Fn import izumi.distage.model.definition.Lifecycle import izumi.functional.quasi.QuasiApplicative -import izumi.distage.model.exceptions.runtime.ProvisioningException import izumi.distage.model.plan.Roots import izumi.functional.bio.data.{Free, FreeError, FreePanic} import izumi.fundamentals.platform.functional.Identity @@ -395,9 +394,7 @@ class ResourceEffectBindingsTest extends AnyWordSpec with MkInjector with GivenW fail(s"Unexpected success! $value") }.unsafeRun() - val exc = intercept[ProvisioningException] { - failure.throwException().unsafeRun() - } + val exc = failure.toThrowable() assert(exc.getMessage.contains("Incompatible effect types")) }