Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement wireResource for scala 2 #175

Merged
merged 32 commits into from
Oct 21, 2021
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
87c4e2a
init work
mbore Aug 4, 2021
d414d7d
wire single resource
mbore Aug 27, 2021
2ed2768
Add support for multiple resources
mbore Aug 27, 2021
c42f895
add wireRec-like composition
mbore Aug 29, 2021
5cc79d4
use default constructors & companion objects
mbore Oct 8, 2021
f4ad58c
Refactor crimpers
mbore Oct 9, 2021
9434160
Fix test
mbore Oct 11, 2021
26bfca2
Rename method
mbore Oct 11, 2021
79f752c
Apply CompanionCrimper changes
mbore Oct 11, 2021
a038fb8
Apply ConstructorCrimper changes
mbore Oct 11, 2021
46b3835
Add some tests for wireApp
adamw Oct 12, 2021
6a9888a
Add assertions to tests, refactor tests
adamw Oct 12, 2021
90b60e4
Add test for IOs
adamw Oct 12, 2021
7e34d33
Add factory testsx
adamw Oct 12, 2021
2b59d75
Failure tests with path
adamw Oct 12, 2021
18cbd08
Move IO wrapped instances out of the scope for the current work
mbore Oct 12, 2021
1373434
Add support for instances
mbore Oct 12, 2021
0d2d0d0
Add basic support for factory methods
mbore Oct 12, 2021
42b5eea
Add todo for resource factory method
mbore Oct 12, 2021
0d797be
Rename
mbore Oct 14, 2021
066f0a9
Update readme
mbore Oct 14, 2021
f190acd
Add IO support
mbore Oct 14, 2021
e71c561
Updated readme
mbore Oct 14, 2021
d08ddfc
Fix typo
mbore Oct 16, 2021
cd9266a
change package name
mbore Oct 16, 2021
5a04285
Add subtype test case
mbore Oct 16, 2021
4e42834
Updated readme
mbore Oct 16, 2021
4232a19
Fix name
mbore Oct 16, 2021
4b70ca2
Add support for subtyping
mbore Oct 16, 2021
2bb2a00
Add tagging example
mbore Oct 16, 2021
53cdea2
change package name
mbore Oct 21, 2021
0446fc3
Add subtype test for resource
mbore Oct 21, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,53 @@ trait UserModule {

This feature is inspired by @yakivy's work on [jam](https://github.com/yakivy/jam).

## Auto wiring

In case you need to build an instance from some particular instances and factory methods it's recommended to use `autowire`. This feature is intended to interpolate with fp libraries (currently we support `cats`).

`autowire` takes as an argument a list which may contain:
* values (e.g. `new A()`)
* factory methods (e.g. `C.create _`)
* cats.effect.Resource (e.g. `cats.effect.Resource[IO].pure(new A())`)
* cats.effect.IO (e.g. `cats.effect.IO.pure(new A())`)
Based on the given list it creates a set of available instances and performs `wireRec` bypassing the instances search phase. The result of the wiring is always wrapped in `cats.effect.Resource`. For example:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if multiple values for a given type are found? E.g. through a value and a resource? I think it should be an error

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently it searches for instances, then resources, then effects. I agree that is should fail in such case, also it may be worth to check if all passed instances/resources/factory methods are used in the generated code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've created #181 & #182 to cover these


```Scala
import cats.effect._

class DatabaseAccess()

class SecurityFilter private (databaseAccess: DatabaseAccess)
object SecurityFilter {
def apply(databaseAccess: DatabaseAccess): SecurityFilter = new SecurityFilter(databaseAccess)
}

class UserFinder(databaseAccess: DatabaseAccess, securityFilter: SecurityFilter)
class UserStatusReader(databaseAccess: DatabaseAccess, userFinder: UserFinder)

object UserModule {
import com.softwaremill.macwire._

val theDatabaseAccess: Resource[IO, DatabaseAccess] = Resource.pure(new DatabaseAccess())

val theUserStatusReader: Resource[IO, UserStatusReader] = autowire[UserStatusReader](theDatabaseAccess)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️ this looks nice :)

}
```

will generate
```Scala
[...]
object UserModule {
import com.softwaremill.macwire._

val theDatabaseAccess: Resource[IO, DatabaseAccess] = Resource.pure(new DatabaseAccess())

val theUserStatusReader: Resource[IO, UserStatusReader] = UserModule.this.theDatabaseAccess.flatMap(
da => Resource.pure[IO, UserStatusReader](new UserStatusReader(da, new UserFinder(da, SecurityFilter.apply(da))))
)
}
```

## Composing modules

Modules (traits or classes containing parts of the object graph) can be combined using inheritance or composition.
Expand Down
21 changes: 20 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ val scalatest = "org.scalatest" %% "scalatest" % "3.2.9"
val javassist = "org.javassist" % "javassist" % "3.28.0-GA"
val akkaActor = "com.typesafe.akka" %% "akka-actor" % "2.6.15"
val javaxInject = "javax.inject" % "javax.inject" % "1"
val cats = "org.typelevel" %% "cats-core" % "2.6.1"
val catsEffect = "org.typelevel" %% "cats-effect" % "3.2.1"

lazy val root = project
.in(file("."))
Expand All @@ -79,7 +81,9 @@ lazy val root = project
testUtil,
utilTests,
macrosAkka,
macrosAkkaTests
macrosAkkaTests,
macrosAutoCats,
macrosAutoCatsTests
).flatMap(_.projectRefs): _*
)

Expand Down Expand Up @@ -167,6 +171,21 @@ lazy val macrosAkkaTests = projectMatrix
.dependsOn(macrosAkka, testUtil)
.jvmPlatform(scalaVersions = scala2)

lazy val macrosAutoCats = projectMatrix
.in(file("macrosAutoCats"))
.settings(commonSettings)
.settings(libraryDependencies ++= Seq(catsEffect, cats))
.dependsOn(macros)
.jvmPlatform(scalaVersions = scala2)
.jsPlatform(scalaVersions = scala2)

lazy val macrosAutoCatsTests = projectMatrix
.in(file("macrosAutoCatsTests"))
.settings(testSettings)
.settings(libraryDependencies ++= Seq(scalatest, catsEffect, tagging))
.dependsOn(macrosAutoCats, testUtil)
.jvmPlatform(scalaVersions = scala2)

Compile / compile := {
// Enabling debug project-wide. Can't find a better way to pass options to scalac.
System.setProperty("macwire.debug", "")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,62 @@ package com.softwaremill.macwire.internals

import scala.reflect.macros.blackbox

private[macwire] class CompanionCrimper [C <: blackbox.Context, T: C#WeakTypeTag](val c: C, log: Logger) {
private[macwire] class CompanionCrimper[C <: blackbox.Context, T: C#WeakTypeTag](val c: C, log: Logger) {
import c.universe._

type DependencyResolverType = DependencyResolver[c.type, Type, Tree]

lazy val targetType: Type = implicitly[c.WeakTypeTag[T]].tpe

lazy val companionType: Option[Type] = if(targetType.companion == NoType) None else Some(targetType.companion)
lazy val companionType: Option[Type] = CompanionCrimper.companionType(c)(targetType)

def isCompanionApply(method: Symbol): Boolean =
lazy val applies: Option[List[Symbol]] = CompanionCrimper.applies(c, log)(targetType)

def applyTree(dependencyResolver: DependencyResolverType): Option[Tree] = CompanionCrimper.applyTree[C](c, log)(targetType, dependencyResolver.resolve(_, _))

}

object CompanionCrimper {
private def showApply[C <: blackbox.Context](c: C)(s: c.Symbol): String = s.asMethod.typeSignature.toString

private def isCompanionApply[C <: blackbox.Context](c: C)(targetType: c.Type, method: c.Symbol): Boolean =
method.isMethod &&
method.isPublic &&
method.asMethod.returnType <:< targetType &&
method.asMethod.name.decodedName.toString == "apply"

lazy val applies: Option[List[Symbol]] = log.withBlock("Looking for apply methods of Companion Object") {
val as: Option[List[Symbol]] = companionType.map(_.members.filter(isCompanionApply).toList)
as.foreach(x => log.withBlock(s"There are ${x.size} apply methods:" ) { x.foreach(c => log(showApply(c))) })
private def companionType[C <: blackbox.Context](c: C)(targetType: c.Type): Option[c.Type] = {
import c.universe._

if(targetType.companion == NoType) None else Some(targetType.companion)
}

private def applies[C <: blackbox.Context](c: C, log: Logger)(targetType: c.Type): Option[List[c.Symbol]] = log.withBlock("Looking for apply methods of Companion Object") {
val as: Option[List[c.Symbol]] = companionType(c)(targetType).map(_.members.filter(CompanionCrimper.isCompanionApply(c)(targetType, _)).toList)
as.foreach(x => log.withBlock(s"There are ${x.size} apply methods:" ) { x.foreach(s => log(showApply(c)(s))) })
as
}

lazy val apply: Option[Symbol] = applies.flatMap( _ match {
case applyMethod :: Nil => Some(applyMethod)
case _ => None
})
def applyTree[C <: blackbox.Context](c: C, log: Logger)(targetType: c.Type, resolver: (c.Symbol, c.Type) => c.Tree): Option[c.Tree] = {
import c.universe._

lazy val applySelect: Option[Select] = apply.map(a => Select(Ident(targetType.typeSymbol.companion), a))
lazy val apply: Option[Symbol] = CompanionCrimper.applies(c, log)(targetType).flatMap( _ match {
case applyMethod :: Nil => Some(applyMethod)
case _ => None
})

lazy val applyParamLists: Option[List[List[Symbol]]] = apply.map(_.asMethod.paramLists)
lazy val applySelect: Option[Select] = apply.map(a => Select(Ident(targetType.typeSymbol.companion), a))

def wireParams(dependencyResolver: DependencyResolverType)(paramLists: List[List[Symbol]]): List[List[Tree]] = paramLists.map(_.map(p => dependencyResolver.resolve(p, p.typeSignature)))
lazy val applyParamLists: Option[List[List[Symbol]]] = apply.map(_.asMethod.paramLists)

def applyArgs(dependencyResolver: DependencyResolverType): Option[List[List[Tree]]] = applyParamLists.map(wireParams(dependencyResolver))
def wireParams(paramLists: List[List[Symbol]]): List[List[Tree]] = paramLists.map(_.map(p => resolver(p, p.typeSignature)))

def applyTree(dependencyResolver: DependencyResolverType): Option[Tree] = for {
pl: List[List[Tree]] <- applyArgs(dependencyResolver)
applyMethod: Tree <- applySelect
} yield pl.foldLeft(applyMethod)((acc: Tree, args: List[Tree]) => Apply(acc, args))
def applyArgs: Option[List[List[Tree]]] = applyParamLists.map(x => wireParams(x))

def showApply(c: Symbol): String = c.asMethod.typeSignature.toString
}
for {
pl: List[List[Tree]] <- applyArgs
applyMethod: Tree <- applySelect
} yield pl.foldLeft(applyMethod)((acc: Tree, args: List[Tree]) => Apply(acc, args))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,98 +2,139 @@ package com.softwaremill.macwire.internals

import scala.reflect.macros.blackbox

private[macwire] class ConstructorCrimper[C <: blackbox.Context, T: C#WeakTypeTag] (val c: C, log: Logger) {
private[macwire] class ConstructorCrimper[C <: blackbox.Context, T: C#WeakTypeTag](val c: C, log: Logger) {
import c.universe._

type DependencyResolverType = DependencyResolver[c.type, Type, Tree]

lazy val typeCheckUtil = new TypeCheckUtil[c.type](c, log)

lazy val targetType: Type = implicitly[c.WeakTypeTag[T]].tpe

// We need to get the "real" type in case the type parameter is a type alias - then it cannot
// be directly instantiated
lazy val targetTypeD: Type = targetType.dealias

lazy val classOfT: c.Expr[Class[T]] = c.Expr[Class[T]](q"classOf[$targetType]")
lazy val constructor: Option[Symbol] = ConstructorCrimper.constructor(c, log)(targetType)

lazy val publicConstructors: Iterable[Symbol] = {
val ctors = targetType.members
.filter(m => m.isMethod && m.asMethod.isConstructor && m.isPublic)
.filterNot(isPhantomConstructor)
log.withBlock(s"There are ${ctors.size} eligible constructors" ) { ctors.foreach(c => log(showConstructor(c))) }
ctors
}
def constructorArgsWithImplicitLookups(dependencyResolver: DependencyResolverType): Option[List[List[Tree]]] =
log.withBlock("Looking for targetConstructor arguments with implicit lookups") {
constructor.map(_.asMethod.paramLists).map(wireConstructorParamsWithImplicitLookups(dependencyResolver))
}

lazy val primaryConstructor: Option[Symbol] = publicConstructors.find(_.asMethod.isPrimaryConstructor)
def constructorTree(dependencyResolver: DependencyResolverType): Option[Tree] =
ConstructorCrimper.constructorTree(c, log)(targetType, dependencyResolver.resolve(_, _))

lazy val injectConstructors: Iterable[Symbol] = {
val isInjectAnnotation = (a: Annotation) => a.toString == "javax.inject.Inject"
val ctors = publicConstructors.filter(_.annotations.exists(isInjectAnnotation))
log.withBlock(s"There are ${ctors.size} constructors annotated with @javax.inject.Inject" ) { ctors.foreach(c => log(showConstructor(c))) }
ctors
}
def wireConstructorParams(
dependencyResolver: DependencyResolverType
)(paramLists: List[List[Symbol]]): List[List[Tree]] = paramLists.map(
_.map(p => dependencyResolver.resolve(p, /*SI-4751*/ ConstructorCrimper.paramType(c)(targetTypeD, p)))
)

lazy val injectConstructor: Option[Symbol] = if(injectConstructors.size > 1) abort(s"Ambiguous constructors annotated with @javax.inject.Inject for type [$targetType]") else injectConstructors.headOption
def wireConstructorParamsWithImplicitLookups(
dependencyResolver: DependencyResolverType
)(paramLists: List[List[Symbol]]): List[List[Tree]] = paramLists.map(_.map {
case i if i.isImplicit => q"implicitly[${ConstructorCrimper.paramType(c)(targetType, i)}]"
case p => dependencyResolver.resolve(p, /*SI-4751*/ ConstructorCrimper.paramType(c)(targetTypeD, p))
})

lazy val constructor: Option[Symbol] = log.withBlock(s"Looking for constructor for $targetType"){
val ctor = injectConstructor orElse primaryConstructor
ctor.foreach(ctor => log(s"Found ${showConstructor(ctor)}"))
ctor
}
}

lazy val constructorParamLists: Option[List[List[Symbol]]] = constructor.map(_.asMethod.paramLists.filterNot(_.headOption.exists(_.isImplicit)))
object ConstructorCrimper {
def showConstructor[C <: blackbox.Context](c: C)(s: c.Symbol): String = s.asMethod.typeSignature.toString

private def constructor[C <: blackbox.Context](c: C, log: Logger)(targetType: c.Type) = {
import c.universe._

/** In some cases there is one extra (phantom) constructor.
* This happens when extended trait has implicit param:
*
* {{{
* trait A { implicit val a = ??? };
* class X extends A
* import scala.reflect.runtime.universe._
* typeOf[X].members.filter(m => m.isMethod && m.asMethod.isConstructor && m.asMethod.isPrimaryConstructor).map(_.asMethod.fullName)
*
* //res1: Iterable[String] = List(X.<init>, A.$init$)
* }}}
*
* The {{{A.$init$}}} is the phantom constructor and we don't want it.
*
* In other words, if we don't filter such constructor using this function
* 'wireActor-12-noPublicConstructor.failure' will compile and throw exception during runtime but we want to fail it during compilation time.
*/
def isPhantomConstructor(constructor: Symbol): Boolean = constructor.asMethod.fullName.endsWith("$init$")

lazy val publicConstructors: Iterable[Symbol] = {
val ctors = targetType.members
.filter(m => m.isMethod && m.asMethod.isConstructor && m.isPublic)
.filterNot(isPhantomConstructor)
log.withBlock(s"There are ${ctors.size} eligible constructors") { ctors.foreach(s => log(showConstructor(c)(s))) }
ctors
}

def constructorArgs(dependencyResolver: DependencyResolverType): Option[List[List[Tree]]] = log.withBlock("Looking for targetConstructor arguments") {
constructorParamLists.map(wireConstructorParams(dependencyResolver))
}
lazy val primaryConstructor: Option[Symbol] = publicConstructors.find(_.asMethod.isPrimaryConstructor)

def constructorArgsWithImplicitLookups(dependencyResolver: DependencyResolverType): Option[List[List[Tree]]] = log.withBlock("Looking for targetConstructor arguments with implicit lookups") {
constructor.map(_.asMethod.paramLists).map(wireConstructorParamsWithImplicitLookups(dependencyResolver))
}
lazy val injectConstructors: Iterable[Symbol] = {
val isInjectAnnotation = (a: Annotation) => a.toString == "javax.inject.Inject"
val ctors = publicConstructors.filter(_.annotations.exists(isInjectAnnotation))
log.withBlock(s"There are ${ctors.size} constructors annotated with @javax.inject.Inject") {
ctors.foreach(s => log(showConstructor(c)(s)))
}
ctors
}

def constructorTree(dependencyResolver: DependencyResolverType): Option[Tree] = log.withBlock(s"Creating Constructor Tree for $targetType"){
val constructionMethodTree: Tree = Select(New(Ident(targetTypeD.typeSymbol)), termNames.CONSTRUCTOR)
constructorArgs(dependencyResolver).map(_.foldLeft(constructionMethodTree)((acc: Tree, args: List[Tree]) => Apply(acc, args)))
lazy val injectConstructor: Option[Symbol] =
if (injectConstructors.size > 1)
c.abort(
c.enclosingPosition,
s"Ambiguous constructors annotated with @javax.inject.Inject for type [$targetType]"
)
else injectConstructors.headOption

log.withBlock(s"Looking for constructor for $targetType") {
val ctor = injectConstructor orElse primaryConstructor
ctor.foreach(ctor => log(s"Found ${showConstructor(c)(ctor)}"))
ctor
}
}

def wireConstructorParams(dependencyResolver: DependencyResolverType)(paramLists: List[List[Symbol]]): List[List[Tree]] = paramLists.map(_.map(p => dependencyResolver.resolve(p, /*SI-4751*/paramType(p))))
private def paramType[C <: blackbox.Context](c: C)(targetTypeD: c.Type, param: c.Symbol): c.Type = {
import c.universe._

def wireConstructorParamsWithImplicitLookups(dependencyResolver: DependencyResolverType)(paramLists: List[List[Symbol]]): List[List[Tree]] = paramLists.map(_.map {
case i if i.isImplicit => q"implicitly[${paramType(i)}]"
case p => dependencyResolver.resolve(p, /*SI-4751*/ paramType(p))
})

private def paramType(param: Symbol): Type = {
val (sym: Symbol, tpeArgs: List[Type]) = targetTypeD match {
case TypeRef(_, sym, tpeArgs) => (sym, tpeArgs)
case t => abort(s"Target type not supported for wiring: $t. Please file a bug report with your use-case.")
case t =>
c.abort(
c.enclosingPosition,
s"Target type not supported for wiring: $t. Please file a bug report with your use-case."
)
}
val pTpe = param.typeSignature.substituteTypes(sym.asClass.typeParams, tpeArgs)
if (param.asTerm.isByNameParam) pTpe.typeArgs.head else pTpe
}

/**
* In some cases there is one extra (phantom) constructor.
* This happens when extended trait has implicit param:
*
* {{{
* trait A { implicit val a = ??? };
* class X extends A
* import scala.reflect.runtime.universe._
* typeOf[X].members.filter(m => m.isMethod && m.asMethod.isConstructor && m.asMethod.isPrimaryConstructor).map(_.asMethod.fullName)
*
* //res1: Iterable[String] = List(X.<init>, A.$init$)
* }}}
*
* The {{{A.$init$}}} is the phantom constructor and we don't want it.
*
* In other words, if we don't filter such constructor using this function
* 'wireActor-12-noPublicConstructor.failure' will compile and throw exception during runtime but we want to fail it during compilation time.
*/
def isPhantomConstructor(constructor: Symbol): Boolean = constructor.asMethod.fullName.endsWith("$init$")

def showConstructor(c: Symbol): String = c.asMethod.typeSignature.toString

def abort(msg: String): Nothing = c.abort(c.enclosingPosition, msg)
def constructorTree[C <: blackbox.Context](
c: C,
log: Logger
)(targetType: c.Type, resolver: (c.Symbol, c.Type) => c.Tree): Option[c.Tree] = {
import c.universe._

lazy val targetTypeD: Type = targetType.dealias

lazy val constructor: Option[Symbol] = ConstructorCrimper.constructor(c, log)(targetType)

lazy val constructorParamLists: Option[List[List[Symbol]]] =
constructor.map(_.asMethod.paramLists.filterNot(_.headOption.exists(_.isImplicit)))

def constructorArgs: Option[List[List[Tree]]] = log.withBlock("Looking for targetConstructor arguments") {
constructorParamLists.map(wireConstructorParams(_))
}

def wireConstructorParams(paramLists: List[List[Symbol]]): List[List[Tree]] =
paramLists.map(_.map(p => resolver(p, /*SI-4751*/ paramType(c)(targetTypeD, p))))

log.withBlock(s"Creating Constructor Tree for $targetType") {
val constructionMethodTree: Tree = Select(New(Ident(targetTypeD.typeSymbol)), termNames.CONSTRUCTOR)
constructorArgs.map(_.foldLeft(constructionMethodTree)((acc: Tree, args: List[Tree]) => Apply(acc, args)))
}
}
}
Loading