Home

Awesome

Jam <img src="https://www.svgrepo.com/show/128194/jam.svg" height="32px" alt="Jam" />

Maven Central Sonatype Nexus (Snapshots) License: MIT <a href="https://typelevel.org/cats/"><img src="https://typelevel.org/cats/img/cats-badge.svg" height="40px" align="right" alt="Cats friendly" /></a>

Jam is an incredibly simple DI Scala library.

Essential differences from macwire:

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

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 _)
}
class QuotaChecker(databaseAccess: DatabaseAccess)

trait QuotaCheckerModule {
    object ResolvedUserModule extends UserModule

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

Implementation details

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

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

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

Changelog

0.4.x

0.3.x

0.2.x:

0.1.x: