yakivy / jam   0.4.5

MIT License GitHub

Incredibly simple DI Scala library.

Scala versions: 3.x 2.13 2.12
Scala.js versions: 1.x
Scala Native versions: 0.4

Jam Jam

Maven Central Sonatype Nexus (Snapshots) License: MIT Cats friendly

Jam is an incredibly simple DI Scala library.

Essential differences from macwire:

  • is simpler and faster, searches candidates only in this
  • supports Scala 3, Scala JS, Scala Native
  • supports macro configuration
  • provides tools for object lifecycle control

Table of contents

  1. Quick start
  2. Brew types
  3. Implementation details
  4. Cats integration
  5. Reval effect
  6. Macro configuration
  7. Troubleshooting
  8. Roadmap
  9. Changelog

Quick start

Latest stable jam dependency:

libraryDependencies += Seq(
    "com.github.yakivy" %%% "jam-core" % "0.4.5",
)

Usage example:

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

trait UserModule {
    val singletonDatabaseAccess = jam.brew[DatabaseAccess]
    val userStatusReader = jam.brewRec[UserStatusReader]
}

Macro output:

trait UserModule {
    val singletonDatabaseAccess = new DatabaseAccess()
    val userStatusReader = new UserStatusReader(
        new UserFinder(
            singletonDatabaseAccess,
            new SecurityFilter(singletonDatabaseAccess),
        )
    )
}

Brew types

  • jam.brew - injects constructor arguments if they are provided in this, otherwise throws an error
  • jam.brewRec - injects constructor arguments if they are provided in this or recursively brews them
  • jam.brewWith - injects lambda arguments if they are provided in this, otherwise throws an error, especially useful when the constructor cannot be resolved automatically:
class PasswordValidator(databaseAccess: DatabaseAccess, salt: String)
object PasswordValidator {
    def create(databaseAccess: DatabaseAccess): PasswordValidator =
        new PasswordValidator(databaseAccess, "salt")
}

trait PasswordValidatorModule extends UserModule {
    val passwordValidator = jam.brewWith(PasswordValidator.create _)
}
  • jam.brewFrom - injects constructor arguments if they are provided in self argument, otherwise throws an error:
class QuotaChecker(databaseAccess: DatabaseAccess)

trait QuotaCheckerModule {
    object ResolvedUserModule extends UserModule

    val quotaChecker = jam.brewFrom[QuotaChecker](ResolvedUserModule)
}

Implementation details

  • injection candidates are being searched in this instance, so to provide an instance for future injection, you need to make it a member of this. Examples:
trait A {
    val a = new A
    ...brewing //val a will be used
}

val container = new {
    val a = new A
    ...brewing //val a will be used
}

trait A {
    def b(): String = {
        val a = new A
        ...brewing //val a will be ignored
    }
}

trait A {
    val a1 = new A
    {
        val a2 = new A
        ...brewing //val a1 will be used
    }
}
  • constructor function is being searched in following order:
    • companion apply method that returns a subtype of brewed type in F[_] context (with jam-cats module)
    • companion apply method that returns a subtype of brewed type
    • class constructor with @Inject annotation
    • class constructor
  • val member works like a singleton provider (instance will be reused for all injections in this score), def member works like a prototype provider (one method call per each injection)
  • library injects only non-implicit constructor arguments; implicits will be resolved by the compiler

Cats integration

jam-cats module provides brewF analogies for all brew methods using cats.Monad typeclass, that allow to brew objects in F[_] context, for example:

trait UserModule {
    val databaseAccess = jam.brew[DatabaseAccess]
    val maybeSecurityFilter = Option(jam.brew[SecurityFilter])
    val maybeUserStatusReader = jam.cats.brewRecF[Option][UserStatusReader]
}

translates to something similar to:

trait UserModule {
    val databaseAccess = new DatabaseAccess()
    val maybeSecurityFilter = Option(new SecurityFilter(databaseAccess))
    val maybeUserStatusReader = (
        Monad[Option].pure(databaseAccess),
        maybeSecurityFilter,
    ).mapN((databaseAccess, securityFilter) => new UserStatusReader(
        new UserFinder(
            databaseAccess,
            securityFilter,
        )
    ))
}

Reval effect

jam-monad module provides Reval effect that encodes the idea of allocating an object which has an associated finalizer. Can be thought of as a mix of cats.effect.Resource and cats.Eval. It can be useful in cases when you need to control an object lifecycle: how many times the object should be allocated, when it should be allocated and how it should be closed. In the combination with jam-cats it should cover most DI cases. For example:

class DatabaseAccess private ()
object DatabaseAccess {
    def apply: Reval[IO, DatabaseAccess] =
        //to allocate instance once on first request (singleton-like)
        Reval.makeThunkLater {
            println("Creating database access")
            new DatabaseAccess()
        }(_ => println("Closing database access"))
}

class SecurityFilter private (val databaseAccess: DatabaseAccess)
object SecurityFilter {
    def apply(databaseAccess: DatabaseAccess): Reval[IO, SecurityFilter] =
        //to allocate instance on every request (prototype-like)
        Reval.makeThunkAlways {
            println("Creating security filter")
            new SecurityFilter(databaseAccess)
        }(_ => println("Closing security filter"))
}

class UserFinder(val databaseAccess: DatabaseAccess, val securityFilter: SecurityFilter)
class OrganizationFinder(val databaseAccess: DatabaseAccess, val securityFilter: SecurityFilter)

trait FinderModule {
    val finders = (
        jam.cats.brewRecF[Reval[IO, *]][UserFinder],
        jam.cats.brewRecF[Reval[IO, *]][OrganizationFinder],
    ).tupled
}

finderModule.finders.usePure.unsafeRunSync()

Will produce the following output:

Creating database access
Creating security filter
Creating security filter
Closing security filter
Closing security filter
Closing database access

Macro configuration

It's also possible to configure brewing behaviour with an implicit macro JamConfig instance, so here is an example if you for example want to limit recursive brewing only to classes that have "brewable" in the name:

object myjam extends jam.core.JamCoreDsl with jam.cats.core.JamCatsDsl {
    //for Scala 2.x
    //and don't forget about Scala 2 macro system requirements:
    //- define macro in a separate compilation unit
    //- add `scala.language.experimental.macros` import
    //- add `org.scala-lang:scala-reflect` compile time dependency
    def myJamConfigImpl(c: blackbox.Context): c.Tree = c.universe.reify {
        new JamConfig(brewRecRegex = "(?i).*brewable.*")
    }.tree
    implicit def myJamConfig: JamConfig = macro myJamConfigImpl

    //for Scala 3.x
    implicit inline def myJamConfig: JamConfig = {
        new JamConfig(brewRecRegex = "(?i).*brewable.*")
    }
}

then myjam.brewRec[WithSingleArg] will throw Recursive brewing for instance (WithSingleArg).a(WithEmptyArgs) is prohibited from config. WithEmptyArgs doesn't match (?i).*brewable.* regex. compilation error.

JamConfig is a dependent type, so any brew methods that is called from myjam object should automatically resolve implicit config without additional imports.

Troubleshooting

  • Scala 2.x compilation of brewWithF methods fails if lambda argument has a closure. For example:
case class A()
case class B()
case class C(a: A, b: B)
object Module {
    val a = Option(A())
    val b = Option(B())
    val c = a.flatMap(a => 
        jam.cats.brewWithF[Option]((b: B) => C(a/*closure*/, b))
    )
}

fails with Error while emitting module.scala, value a. I don't expect compiler team to fix this issue, because macros system was fully rewritten in Scala 3. As a workaround you can create an object manually or move the closure out of brewWithF:

val c = a.flatMap(a => 
    jam.cats.brewWithF[Option]((b: B) => C(_, b)).map(_.apply(a)/*closure*/)
)

Roadmap

  • fix error message on vacancy for brewF
  • extract annotation pattern (instead of hardcoded javax.inject.Inject) for constructor selection to macro config
  • extract method pattern (instead of hardcoded apply) for companion constructor selection to macro config
  • resolve generic apply method if generics are the same to class constructor

Changelog

0.4.x

  • add jam.monad.Reval effect
  • resolve constructor from companion object
  • fix implicit args resolution for jam-cats
  • fix candidates type resolution for Scala 2.x
  • fix a bunch of error messages
  • allow to brew from case classes

0.3.x

  • add jam.cats module
  • rename brewWithFrom to brewFromWith and swap arguments
  • a couple of minor fixes

0.2.x:

  • add brewing configuration: JamConfig
  • add member names for ambiguous candidates compilation error
  • optimize compilation time for Scala 2.x
  • throw compilation error if member type cannot be resolved

0.1.x:

  • fix import ambiguity: jam.tree.brew was renamed to jam.brewRec
  • fix method overload ambiguity: jam.brew(f) was renamed to jam.brewWith(f)
  • allow passing an instance to brew from (instead of this): jam.brewFrom
  • various refactorings and cleanups