diff --git a/.gitignore b/.gitignore index 3093b60a..7b3fcca5 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ out .vscode metals.sbt Workpad.scala -.cursor \ No newline at end of file +.cursor +.scala-build \ No newline at end of file diff --git a/README.md b/README.md index 1ccf1b09..ad84a9de 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ There's a couple of wiring variants that you can choose from: * `wire` create an instance of the given type, using dependencies from the context, within which it is called. Dependencies are looked up in the enclosing trait/class/object and parents (via inheritance). * `wireRec` is a variant of `wire`, which creates missing dependencies using constructors. +* `wireSet` collect all instances of the given type from the context and return them as a `Set`. +* `wireList` collect all instances of the given type from the context and return them as a `List`, preserving order. In other words, `autowire` is context-free, while the `wire` family of macros is context-dependent. @@ -56,7 +58,7 @@ To use, add the following dependency: - [Accessing wired instances dynamically](#accessing-wired-instances-dynamically) - [Limitations](#limitations) - [Akka integration](#akka-integration) - - [Multi Wiring (wireSet)](#multi-wiring-wireset) + - [Multi Wiring (wireSet and wireList)](#multi-wiring-wireset-and-wirelist) - [Autowire for cats-effect](#autowire-for-cats-effect) - [Interceptors](#interceptors) - [Qualifiers](#qualifiers) @@ -724,15 +726,18 @@ object UserFinderActor { val theUserFinder = wireActorWith(UserFinderActor.get _)("userFinder") ``` -## Multi Wiring (wireSet) +## Multi Wiring (wireSet and wireList) Using `wireSet` you can obtain a set of multiple instances of the same type. This is done without constructing the set explicitly. All instances of the same type which are found by MacWire are used to construct the set. -Consider the below example. Let's suppose that you want to create a `RockBand(musicians: Set[Musician])` object. It's easy to do so using the `wireSet` functionality: +Using `wireList` you can obtain a list of multiple instances of the same type, preserving the order of definition. This works similarly to `wireSet`, but returns a `List` instead of a `Set`, maintaining the order in which the instances are discovered during macro expansion. This method is available only in Scala 3. + +Consider the below example. Let's suppose that you want to create a `RockBand` object with musicians: ```scala trait Musician class RockBand(musicians: Set[Musician]) +class Orchestra(musicians: List[Musician]) trait RockBandModule { lazy val singer = new Musician {} @@ -740,13 +745,24 @@ trait RockBandModule { lazy val drummer = new Musician {} lazy val bassist = new Musician {} - lazy val musicians = wireSet[Musician] // all above musicians will be wired together - // musicians has type Set[Musician] + lazy val musiciansSet = wireSet[Musician] // all above musicians will be wired together + // musiciansSet has type Set[Musician] (unordered) + + lazy val musiciansList = wireList[Musician] // all above musicians will be wired together + // musiciansList has type List[Musician] (preserves order) lazy val rockBand = wire[RockBand] + lazy val orchestra = wire[Orchestra] } ``` +Both `wireSet` and `wireList` look for instances in the same places: +- enclosing members (lazy vals, vals, defs without parameters) +- enclosing imports +- parent classes and traits + +The key difference is that `wireSet` returns an unordered `Set[T]` while `wireList` returns an ordered `List[T]` that preserves the order of definition discovery. + # Autowire for cats-effect ![Scala 2](https://img.shields.io/badge/Scala%202-8A2BE2) diff --git a/macros/src/main/scala-3/com/softwaremill/macwire/internals/EligibleValuesFinder.scala b/macros/src/main/scala-3/com/softwaremill/macwire/internals/EligibleValuesFinder.scala index e877599a..906f7062 100644 --- a/macros/src/main/scala-3/com/softwaremill/macwire/internals/EligibleValuesFinder.scala +++ b/macros/src/main/scala-3/com/softwaremill/macwire/internals/EligibleValuesFinder.scala @@ -92,7 +92,7 @@ private[macwire] class EligibleValuesFinder[Q <: Quotes](log: Logger)(using val addTo.find(_ == t).fold(t :: addTo)(_ => addTo) } - trees.foldLeft(List.empty[Tree])(addIfUnique) + trees.foldLeft(List.empty[Tree])(addIfUnique).reverse // preserve order } def findInScope(tpe: TypeRepr, scope: Scope): Iterable[Tree] = { @@ -116,7 +116,7 @@ private[macwire] class EligibleValuesFinder[Q <: Quotes](log: Logger)(using val def findInAllScope(tpe: TypeRepr): Iterable[Tree] = { @tailrec def accInScope(scope: Scope, acc: List[Tree]): List[Tree] = { - val newAcc = doFindInScope(tpe, scope) ++ acc + val newAcc = acc ++ doFindInScope(tpe, scope) if (!scope.isMax) accInScope(scope.widen, newAcc) else newAcc } uniqueTrees(accInScope(Scope.init, Nil)) @@ -163,7 +163,6 @@ private[macwire] class EligibleValuesFinder[Q <: Quotes](log: Logger)(using val private def buildEligibleValue(symbol: Symbol, scope: Scope): PartialFunction[Tree, EligibleValues] = { case m: ValDef => merge( - inspectModule(scope.widen, m.rhs.map(_.tpe).getOrElse(m.tpt.tpe), m), EligibleValues( Map( scope -> List( @@ -173,11 +172,11 @@ private[macwire] class EligibleValuesFinder[Q <: Quotes](log: Logger)(using val ) ) ) - ) + ), + inspectModule(scope.widen, m.rhs.map(_.tpe).getOrElse(m.tpt.tpe), m) ) case m: DefDef if m.termParamss.flatMap(_.params).isEmpty => merge( - inspectModule(scope.widen, m.rhs.map(_.tpe).getOrElse(m.returnTpt.tpe), m), EligibleValues( Map( scope -> List( @@ -187,7 +186,8 @@ private[macwire] class EligibleValuesFinder[Q <: Quotes](log: Logger)(using val ) ) ) - ) + ), + inspectModule(scope.widen, m.rhs.map(_.tpe).getOrElse(m.returnTpt.tpe), m) ) } @@ -196,7 +196,12 @@ private[macwire] class EligibleValuesFinder[Q <: Quotes](log: Logger)(using val ) def merge(ev1: EligibleValues, ev2: EligibleValues): EligibleValues = - EligibleValues((ev1.values.toSeq ++ ev2.values.toSeq).groupBy(_._1).view.mapValues(_.flatMap(_._2).toList).toMap) + EligibleValues( + ( + for key <- ev1.values.keySet ++ ev2.values.keySet + yield key -> (ev1.values.getOrElse(key, Nil) ++ ev2.values.getOrElse(key, Nil)) + ).toMap + ) } } diff --git a/macros/src/main/scala-3/com/softwaremill/macwire/internals/MacwireMacros.scala b/macros/src/main/scala-3/com/softwaremill/macwire/internals/MacwireMacros.scala index 14eed22b..0636d9b1 100644 --- a/macros/src/main/scala-3/com/softwaremill/macwire/internals/MacwireMacros.scala +++ b/macros/src/main/scala-3/com/softwaremill/macwire/internals/MacwireMacros.scala @@ -87,13 +87,17 @@ object MacwireMacros { code } - def wireSet_impl[T: Type](using q: Quotes): Expr[Set[T]] = { + private def wireCollInstances[T: Type](using q: Quotes) = { import q.reflect.* val tpe = TypeRepr.of[T] val dependencyResolver = DependencyResolver.throwErrorOnResolutionFailure[q.type, T](log) - val instances = dependencyResolver.resolveAll(tpe) + dependencyResolver.resolveAll(tpe) + } + + def wireSet_impl[T: Type](using q: Quotes): Expr[Set[T]] = { + val instances = wireCollInstances[T] val code = '{ ${ Expr.ofSeq(instances.toSeq.map(_.asExprOf[T])) }.toSet } @@ -101,4 +105,13 @@ object MacwireMacros { code } + def wireList_impl[T: Type](using q: Quotes): Expr[List[T]] = { + val instances = wireCollInstances[T] + + val code = '{ ${ Expr.ofSeq(instances.toSeq.map(_.asExprOf[T])) }.toList } + + log(s"Generated code: ${code.show}") + code + } + } diff --git a/macros/src/main/scala-3/com/softwaremill/macwire/macwire.scala b/macros/src/main/scala-3/com/softwaremill/macwire/macwire.scala index 57de7a00..fe8c579a 100644 --- a/macros/src/main/scala-3/com/softwaremill/macwire/macwire.scala +++ b/macros/src/main/scala-3/com/softwaremill/macwire/macwire.scala @@ -31,6 +31,11 @@ inline def wire[T]: T = ${ MacwireMacros.wireImpl[T] } /** Collect all instances of the given type, available in the surrounding context (trait/class/object). */ inline def wireSet[T]: Set[T] = ${ MacwireMacros.wireSet_impl[T] } +/** Collect all instances of the given type, available in the surrounding context (trait/class/object), preserving the + * order of definition. + */ +inline def wireList[T]: List[T] = ${ MacwireMacros.wireList_impl[T] } + inline def wireWith[RES](inline factory: () => RES): RES = ${ MacwireMacros.wireWith_impl[RES]('factory) } inline def wireWith[A, RES](inline factory: (A) => RES): RES = ${ MacwireMacros.wireWith_impl[RES]('factory) } inline def wireWith[A, B, RES](inline factory: (A, B) => RES): RES = ${ MacwireMacros.wireWith_impl[RES]('factory) } diff --git a/tests/src/test/resources/test-cases/wireList.scala3.success b/tests/src/test/resources/test-cases/wireList.scala3.success new file mode 100644 index 00000000..89e3152d --- /dev/null +++ b/tests/src/test/resources/test-cases/wireList.scala3.success @@ -0,0 +1,43 @@ +trait A +class A1 extends A +class A2 extends A +class A3 extends A +class A4 extends A + +case class Group(as: List[A]) + +class Plugin1 { + lazy val a1: A = wire[A1] +} + +class Plugin2 { + lazy val a2: A = wire[A2] +} + +trait Module3 { + lazy val a3: A = wire[A3] +} + +class App(plugin1: Plugin1, plugin2: Plugin2) extends Module3 { + import plugin1._ + import plugin2._ + + lazy val a4: A = wire[A4] + + // wireList looks at the same places as wireSet: + // - enclosing members + // - enclosing imports + // - parents + // but preserves the order of definition + lazy val as: List[A] = wireList[A] + + // inject that List of A into Group + lazy val group: Group = wire[Group] +} + +val plugin1 = new Plugin1 +val plugin2 = new Plugin2 +val app = new App(plugin1, plugin2) + +require(app.group.as == List(plugin1.a1, plugin2.a2, app.a3, app.a4)) + diff --git a/util-tests/src/test/resources/test-cases/moduleWireList.scala3.success b/util-tests/src/test/resources/test-cases/moduleWireList.scala3.success new file mode 100644 index 00000000..8f98799e --- /dev/null +++ b/util-tests/src/test/resources/test-cases/moduleWireList.scala3.success @@ -0,0 +1,33 @@ +#include commonSimpleClasses + +@Module class ModuleA1 { lazy val a: A = wire[A] } +@Module class ModuleA2 { lazy val a: A = wire[A] } + +class AProvider { lazy val a: A = wire[A] } + +class App(ma1: ModuleA1, + ma2: ModuleA2) { + + lazy val as: List[A] = wireList[A] // should look into modules and preserve order +} + +class App2(ma1: ModuleA1, aProvider: AProvider) { + + // local definitions or imports shouldn't shadow modules + import aProvider.{a => aProvided} + lazy val a: A = wire[A] + + lazy val as: List[A] = wireList[A] // => List(a, ma1.a, aProvided) in some deterministic order +} + +object Test { + val ma1 = wire[ModuleA1] + val ma2 = wire[ModuleA2] + val aProvider = wire[AProvider] + val app = wire[App] + val app2 = wire[App2] +} + +// Test that wireList finds the expected instances (same as wireSet but as List) +require(Test.app.as == List(Test.ma1.a, Test.ma2.a)) +require(Test.app2.as == List(Test.ma1.a, Test.app2.a, Test.aProvider.a))