Skip to content

Commit a8cfd4b

Browse files
authored
DSL: Fix .addDependency not working with resource bindings. Throw when .modify/.annotateParameter are used on non-function bindings instead of silently creating a mutator. Throw when parameter is not found in .annotateParameter instead of doing nothing. (#2255)
* DSL: Fix .addDependency not working with resource bindings. Throw when .modify/.annotateParameter are used on non-function bindings instead of silently creating a mutator. Throw when parameter is not found in .annotateParameter instead of doing nothing. * fix build * In `annotateParameter`, throw by default if parameter doesn't exist, move old behaviour to `annotateParameterIfExists`
1 parent 91e0cae commit a8cfd4b

File tree

8 files changed

+154
-31
lines changed

8 files changed

+154
-31
lines changed

distage/distage-core-api/src/main/scala/izumi/distage/model/definition/dsl/AbstractBindingDefDSL.scala

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -300,28 +300,30 @@ object AbstractBindingDefDSL {
300300
}
301301

302302
override protected def _modifyBy(f: Functoid[T] => Functoid[T]): ModifyTaggingDSL[T] = by(f)
303+
override protected def _addDependencies(keys: Iterable[DIKey]): ModifyTaggingDSL[T] = by(_.addDependencies(keys))
303304
}
304305

305306
trait AddDependencyDSL[T, Self] extends Any {
306307
protected def _modifyBy(f: Functoid[T] => Functoid[T]): Self
308+
protected def _addDependencies(keys: Iterable[DIKey]): Self
307309

308-
def addDependency[B: Tag]: Self = {
309-
_modifyBy(_.addDependency[B])
310+
final def addDependency[B: Tag]: Self = {
311+
addDependency(DIKey[B])
310312
}
311313

312-
def addDependency[B: Tag](name: Identifier): Self = {
313-
_modifyBy(_.addDependency[B](name))
314+
final def addDependency[B: Tag](name: Identifier): Self = {
315+
addDependency(DIKey[B](name))
314316
}
315317

316-
def addDependency(key: DIKey): Self = {
317-
_modifyBy(_.addDependency(key))
318+
final def addDependency(key: DIKey): Self = {
319+
_addDependencies(key :: Nil)
318320
}
319321

320-
def addDependencies(keys: Iterable[DIKey]): Self = {
321-
_modifyBy(_.addDependencies(keys))
322+
final def addDependencies(keys: Iterable[DIKey]): Self = {
323+
_addDependencies(keys)
322324
}
323325

324-
def annotateParameter[P: Tag](name: Identifier): Self = {
326+
final def annotateParameter[P: Tag](name: Identifier): Self = {
325327
_modifyBy(_.annotateParameter[P](name))
326328
}
327329
}
@@ -390,7 +392,8 @@ object AbstractBindingDefDSL {
390392
case _: SetId => 0
391393
case _: SetIdFromImplName => 1
392394
case _: Modify[?] => 2
393-
case _: AliasTo => 3
395+
case _: AddDependencies => 3
396+
case _: AliasTo => 4
394397
}
395398
sortedOps.foreach {
396399
case SetImpl(implDef) =>
@@ -403,19 +406,33 @@ object AbstractBindingDefDSL {
403406
case SetIdFromImplName() =>
404407
b = b.withTarget(DIKey.IdKey(b.key.tpe, b.implementation.implType.tag.longNameWithPrefix.toLowerCase))
405408
case Modify(functoidModifier: (Functoid[t] => Functoid[u])) =>
409+
b = b.withImplDef(b.implementation match {
410+
case implDef: ImplDef.ProviderImpl =>
411+
applyFunctoidModifier(implDef, functoidModifier)
412+
case ImplDef.ResourceImpl(implType, effectHKTypeCtor, resourceImpl: ImplDef.ProviderImpl) =>
413+
ImplDef.ResourceImpl(implType, effectHKTypeCtor, applyFunctoidModifier(resourceImpl, functoidModifier))
414+
case ImplDef.EffectImpl(implType, effectHKTypeCtor, effectImpl: ImplDef.ProviderImpl) =>
415+
ImplDef.EffectImpl(implType, effectHKTypeCtor, applyFunctoidModifier(effectImpl, functoidModifier))
416+
case _ =>
417+
throw new InvalidFunctoidModifier(
418+
s"""Cannot apply Functoid modifier $functoidModifier to binding $b - Functoid is inaccessible in binding implementation. Expected `ImplDef.ProviderImpl`, but got `ImplDef.${b.implementation.productPrefix}`
419+
| Please use a separate mutator binding `modify[T].by { <your-modifier> }` instead. (${initial.origin})""".stripMargin
420+
)
421+
})
422+
case AddDependencies(dependencies) =>
423+
val functoidModifier = (_: Functoid[Any]).addDependencies(dependencies)
406424
b.implementation match {
407-
case ImplDef.ProviderImpl(implType, function) =>
408-
val newProvider = functoidModifier(Functoid(function)).get
409-
if (newProvider.ret <:< implType) {
410-
b = b.withImplDef(ImplDef.ProviderImpl(implType, newProvider))
411-
} else {
412-
throw new InvalidFunctoidModifier(
413-
s"Cannot apply invalid Functoid modifier $functoidModifier, new return type `${newProvider.ret}` is not a subtype of the old return type `${function.ret}` (${initial.origin})"
414-
)
415-
}
425+
case providerImpl: ImplDef.ProviderImpl =>
426+
b = b.withImplDef(applyFunctoidModifier(providerImpl, functoidModifier))
427+
case ImplDef.ResourceImpl(implType, effectHKTypeCtor, resourceImpl: ImplDef.ProviderImpl) =>
428+
b = b.withImplDef(ImplDef.ResourceImpl(implType, effectHKTypeCtor, applyFunctoidModifier(resourceImpl, functoidModifier)))
429+
case ImplDef.EffectImpl(implType, effectHKTypeCtor, effectImpl: ImplDef.ProviderImpl) =>
430+
b = b.withImplDef(ImplDef.EffectImpl(implType, effectHKTypeCtor, applyFunctoidModifier(effectImpl, functoidModifier)))
416431
case _ =>
417432
// add an independent mutator instead of modifying the original functoid, if no original functoid is available
418-
val newProvider = functoidModifier(Functoid.identityKey[t](b.key)).get
433+
// this is ok for `addDependencies` because we don't need to access/modify arguments of the original functoid,
434+
// which might be necessary for a general functoid modifier such as `annotateParameter`.
435+
val newProvider = Functoid.identityKey[Any](b.key).addDependencies(dependencies).get
419436
val newRef = SingletonBinding(b.key, ImplDef.ProviderImpl(newProvider.ret, newProvider), Set.empty, b.origin, isMutator = true)
420437
refs = newRef :: refs
421438
}
@@ -436,6 +453,18 @@ object AbstractBindingDefDSL {
436453
ops += op
437454
this
438455
}
456+
457+
private def applyFunctoidModifier[A, B](implDef: ImplDef.ProviderImpl, functoidModifier: Functoid[A] => Functoid[B]): ImplDef.ProviderImpl = {
458+
val ImplDef.ProviderImpl(implType, function) = implDef
459+
val newProvider = functoidModifier(Functoid(function)).get
460+
if (newProvider.ret <:< implType) {
461+
ImplDef.ProviderImpl(implType, newProvider)
462+
} else {
463+
throw new InvalidFunctoidModifier(
464+
s"Cannot apply invalid Functoid modifier $functoidModifier, new return type `${newProvider.ret}` is not a subtype of the old return type `${function.ret}` (${initial.origin})"
465+
)
466+
}
467+
}
439468
}
440469

441470
final class SetRef(initial: EmptySetBinding[DIKey.TypeKey]) extends BindingRef {
@@ -558,6 +587,7 @@ object AbstractBindingDefDSL {
558587
final case class SetId(id: Identifier) extends SingletonInstruction
559588
final case class SetIdFromImplName() extends SingletonInstruction
560589
final case class Modify[T](functoidModifier: Functoid[T] => Functoid[T]) extends SingletonInstruction
590+
final case class AddDependencies(dependencies: Iterable[DIKey]) extends SingletonInstruction
561591
final case class AliasTo(key: DIKey.BasicKey, pos: SourceFilePosition) extends SingletonInstruction
562592
}
563593

distage/distage-core-api/src/main/scala/izumi/distage/model/definition/dsl/ModuleDefDSL.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,10 @@ object ModuleDefDSL {
791791

792792
override protected def _modifyBy(f: Functoid[T] => Functoid[T]): Self = modifyBy(f)
793793

794+
override protected def _addDependencies(keys: Iterable[DIKey]): Self = {
795+
addOp(AddDependencies(keys))(toSame)
796+
}
797+
794798
}
795799

796800
final class SetDSL[T](

distage/distage-core/src/main/scala/izumi/distage/modules/support/AnyBIOSupportModule.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class AnyBIOSupportModule[F[+_, +_]: TagKK](implicit t: TagK[F[Throwable, _]], t
3030

3131
make[QuasiIORunner2[F]]
3232
.from[QuasiIORunner.BIOImpl[F]]
33-
.annotateParameter[ExecutionContext]("cpu") // scala.js
33+
.modifyBy(_.annotateParameterIfExists[ExecutionContext]("cpu")) // scala.js
3434

3535
make[QuasiIO2[F]]
3636
.aliased[QuasiPrimitives2[F]]

distage/distage-core/src/test/scala/izumi/distage/dsl/DSLTest.scala

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import izumi.distage.model.definition.Binding.{SetElementBinding, SingletonBindi
99
import izumi.distage.model.definition.StandardAxis.{Mode, Repo}
1010
import izumi.distage.model.definition.dsl.IncludesDSL.TagMergePolicy
1111
import izumi.distage.model.definition.{Binding, BindingOrigin, BindingTag, Bindings, ImplDef, Lifecycle, Module, ModuleBase}
12+
import izumi.distage.model.exceptions.dsl.{InvalidFunctoidModifier, ParameterNotFoundForAnnotation}
1213
import izumi.distage.model.planning.PlanIssue
1314
import izumi.fundamentals.platform.functional.Identity
1415
import izumi.fundamentals.platform.language.SourceFilePosition
@@ -806,7 +807,67 @@ class DSLTest extends AnyWordSpec with MkInjector with should.Matchers {
806807
assert(verification.verificationFailed)
807808
assert(verification.issues.get.forall(_.isInstanceOf[PlanIssue.MissingImport]))
808809
val imports = verification.issues.get.toSet.collect { case i: PlanIssue.MissingImport => (i.dependee, i.key) }
809-
assert(imports == Set(DIKey[Int] -> DIKey[String], DIKey[Unit]("x") -> DIKey[String]))
810+
assert(
811+
imports == Set(
812+
DIKey[Int] -> DIKey[String],
813+
DIKey[Unit]("x") -> DIKey[String],
814+
)
815+
)
816+
// two modifier bindings were added
817+
assert(definition.bindings.size == (3 + 2))
818+
}
819+
820+
"addDependency supports adding dependencies for .fromResource/.fromEffect bindings" in {
821+
val definition = new ModuleDef {
822+
make[Int].fromResource(Lifecycle.pure(5)).addDependency[String]
823+
make[Long].fromResource(() => Lifecycle.pure(5L)).addDependency[String]
824+
make[Short].fromEffect[Identity, Short](() => 5: Identity[Short]).addDependency[String]
825+
}
826+
827+
val verification = PlanVerifier().verify[Identity](definition, Roots.Everything, Set.empty, Set.empty)
828+
assert(verification.verificationFailed)
829+
assert(verification.issues.get.forall(_.isInstanceOf[PlanIssue.MissingImport]))
830+
val imports = verification.issues.get.toSet.collect { case i: PlanIssue.MissingImport => (i.dependee, i.key) }
831+
assert(
832+
imports == Set(
833+
DIKey[Int] -> DIKey[String],
834+
DIKey.ResourceKey(DIKey[Long], SafeType.get[Lifecycle[Identity, Long]]) -> DIKey[String],
835+
DIKey.EffectKey(DIKey[Short], SafeType.get[Short]) -> DIKey[String],
836+
)
837+
)
838+
// only one modifier binding was added for .fromValue-like make[Int] binding
839+
assert(definition.bindings.size == (3 + 1))
840+
}
841+
842+
"modify & annotateParameter do not support adding dependencies for .fromValue and .using bindings" in {
843+
intercept[InvalidFunctoidModifier](new ModuleDef {
844+
make[Int]
845+
.fromValue(5)
846+
.modifyBy(_.addDependency[String])
847+
}.bindings)
848+
intercept[InvalidFunctoidModifier](new ModuleDef {
849+
make[Unit].fromValue(())
850+
make[Unit].named("x").using[Unit].annotateParameter[String]("special")
851+
}.bindings)
852+
}
853+
854+
"annotateParameter throws when annotating non-existent parameters" in {
855+
val okDef = new ModuleDef {
856+
make[Int]
857+
.from((_: Long).toInt)
858+
.annotateParameter[Long]("special")
859+
}
860+
assert(okDef.bindings != null)
861+
862+
val badDef = new ModuleDef {
863+
make[Int]
864+
.from((_: Long).toInt)
865+
.annotateParameter[String]("special")
866+
}
867+
val err = intercept[ParameterNotFoundForAnnotation](badDef.bindings)
868+
assert(err.getMessage contains "Long")
869+
assert(err.getMessage contains "special")
870+
assert(err.getMessage contains "String")
810871
}
811872

812873
}

distage/distage-framework-docker/src/main/scala/izumi/distage/docker/DockerContainer.scala

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,21 +94,17 @@ object DockerContainer {
9494
selfTag: distage.Tag[DockerContainer[T]],
9595
mutateModule: ModuleDefDSL#MutationContext,
9696
): Functoid[ContainerResource[F, T]] = {
97-
addContainerDependency[containerDecl.Tag]
98-
self
99-
.addDependency[DockerContainer[containerDecl.Tag]]
100-
.annotateParameter[Set[DockerContainer[Any]]](DependencyTag.get[containerDecl.Tag])
97+
addContainerToDependenciesSet[containerDecl.Tag]
98+
self.addDependency[DockerContainer[containerDecl.Tag]]
10199
}
102100

103101
def dependOnContainer[T2](
104102
implicit tag: distage.Tag[DockerContainer[T2]],
105103
selfTag: distage.Tag[DockerContainer[T]],
106104
mutateModule: ModuleDefDSL#MutationContext,
107105
): Functoid[ContainerResource[F, T]] = {
108-
addContainerDependency[T2]
109-
self
110-
.addDependency[DockerContainer[T2]]
111-
.annotateParameter[Set[DockerContainer[Any]]](DependencyTag.get[T2])
106+
addContainerToDependenciesSet[T2]
107+
self.addDependency[DockerContainer[T2]]
112108
}
113109

114110
/**
@@ -183,7 +179,7 @@ object DockerContainer {
183179
}
184180
}
185181

186-
private def addContainerDependency[T2](
182+
private def addContainerToDependenciesSet[T2](
187183
implicit tag: distage.Tag[DockerContainer[T2]],
188184
selfTag: distage.Tag[DockerContainer[T]],
189185
mutateModule: ModuleDefDSL#MutationContext,

distage/distage-framework/.jvm/src/main/scala/izumi/distage/framework/services/ResourceRewriter.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class ResourceRewriter(
5555

5656
private def rewrite[TGT](tgt: SafeType, resourceType: SafeType)(convert: TGT => Lifecycle[Identity, TGT])(b: Binding): Seq[Binding] = {
5757
b match {
58+
case b if b.isMutator => Seq(b) // do not rewrite mutators
5859
case implBinding: Binding.ImplBinding =>
5960
implBinding match {
6061
case binding: Binding.SingletonBinding[?] =>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package izumi.distage.model.exceptions.dsl
2+
3+
class ParameterNotFoundForAnnotation(message: String) extends RuntimeException(message)

fundamentals/fundamentals-functoid/src/main/scala/izumi/distage/model/providers/AbstractFunctoid.scala

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package izumi.distage.model.providers
22

33
import izumi.distage.model.definition.Identifier
4+
import izumi.distage.model.exceptions.dsl.ParameterNotFoundForAnnotation
45
import izumi.distage.model.reflection.{DIKey, Provider, SafeType}
56
import izumi.fundamentals.platform.language.Quirks.Discarder
7+
import izumi.fundamentals.preamble.toRichIterable
68
import izumi.reflect.Tag
79

810
import scala.annotation.unchecked.uncheckedVariance
@@ -65,21 +67,47 @@ trait AbstractFunctoid[+A, Ftoid[+X] <: AbstractFunctoid[X, Ftoid]] {
6567
* Add an `@Id` annotation to an unannotated parameter `P`, e.g.
6668
* for .annotateParameter[P]("my-id"), transform lambda `(p: P) => x(p)`
6769
* into `(p: P @Id("my-id")) => x(p)`
70+
*
71+
* @throws ParameterNotFoundForAnnotation if there's no unannotated parameter `p: P` in functoid
6872
*/
6973
def annotateParameter[P: Tag](name: Identifier): Ftoid[A] = {
74+
val newFn = annotateParameterIfExists[P](name)
75+
if (newFn.get.parameters == this.get.parameters) {
76+
throw new ParameterNotFoundForAnnotation(
77+
s"""Could not annotate parameter `${DIKey[P]}` with annotation `${name.idContract.repr(name.id)}`:
78+
|Parameter `${DIKey[P]}` not found.
79+
|Found other parameters:${this.get.parameters.map(_.key).niceList()}
80+
|In Functoid: `$this`
81+
|Use `annotateParameterIfExists` if the parameter missing is not an error in your circumstance""".stripMargin
82+
)
83+
} else {
84+
newFn
85+
}
86+
}
87+
88+
/**
89+
* Add an `@Id` annotation to an unannotated parameter `P`, e.g.
90+
* for .annotateParameter[P]("my-id"), transform lambda `(p: P) => x(p)`
91+
* into `(p: P @Id("my-id")) => x(p)`
92+
*
93+
* Does nothing if there's no unannotated parameter `p: P` in functoid
94+
*/
95+
def annotateParameterIfExists[P: Tag](name: Identifier): Ftoid[A] = {
7096
val paramTpe = SafeType.get[P]
7197
annotateParameterWhen(name) {
7298
case DIKey.TypeKey(tpe, _) => tpe == paramTpe
7399
case _: DIKey.IdKey[?] => false
74100
}
75101
}
102+
76103
/** Add an `@Id(name)` annotation to all unannotated parameters */
77104
def annotateAllParameters(name: Identifier): Ftoid[A] = {
78105
annotateParameterWhen(name) {
79106
case _: DIKey.TypeKey => true
80107
case _: DIKey.IdKey[?] => false
81108
}
82109
}
110+
83111
/** Add an `@Id(name)` annotation to all parameters matching `predicate` */
84112
def annotateParameterWhen(name: Identifier)(predicate: DIKey.BasicKey => Boolean): Ftoid[A] = {
85113
val newProvider = this.get.replaceKeys {

0 commit comments

Comments
 (0)