Skip to content

Commit

Permalink
Merge pull request #175 from softwaremill/simplified-wireResoure-scala2
Browse files Browse the repository at this point in the history
Implement wireResource for scala 2
  • Loading branch information
adamw authored Oct 21, 2021
2 parents 9f7c15d + 0446fc3 commit 5b0f8fc
Show file tree
Hide file tree
Showing 29 changed files with 822 additions and 88 deletions.
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,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:

```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)
}
```

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

0 comments on commit 5b0f8fc

Please sign in to comment.