Giter Club home page Giter Club logo

jam's Introduction

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

jam's People

Contributors

denvercoder10 avatar limpid-kzonix avatar yakivy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

jam's Issues

Can't handle type parameters (scala 2)

class A[B]()
trait Module {
  val a: A[String] = jam.brew[A[String]]
}

Expected: This works
Actual: Unable to find public constructor for (A[String])

Related:

class A[B](b: B)
trait Module {
  val string: String = ""
  val a: A[String] = jam.brew[A[String]]
}

Expected: This works
Actual: Unable to find public constructor for (A[String])

[Feature Request] Can't handle classes with more than one constructor

It'd be cool if jam respected the @Inject annotation (or had some other way to specify a constructor to use):

import javax.inject.Inject

class A(b: Int) {
  @Inject
  def this(c: String) = this(c.length)
}
trait Module {
  val c: String = ""
  val a: A = jam.brew[A]
}

Desired: This works, and uses the constructor annotated with @Inject
Actual: More than one primary constructor was found for (A)

Can't inject Strings (scala 2)

class A(b: String)
trait Module {
  val string: String = ""
  val a: A = jam.brew[A]
}

Expected: This works
Actual: More than one injection candidate was found for (A).b

I added parameterCandidates to this error message locally (note: This may be useful in general as it makes debugging this error much easier) and saw that it had List((value string,String("")), (method toString,(): String)). The toString method probably shouldn't be considered a candidate for injection

[Feature request] Allow values of type B to be injected for constructor args of type A if B <: A

It would be nice to be able to automatically inject values of a subclass type for constructor args that are of the superclass type.

Here's a minimal example:

class A()
class B extends A()
class C(a: A)

trait TestModule {
  // would be nice if this is all we needed to provide an A
  val b: B = jam.brew[B]
  // If the following line is uncommented, compilation works, since we have an A in scope
  // val a: A = b
  val c: C = jam.brew[C]
}

Error messages when using type tagging are confusing

import com.softwaremill.tagging._

trait Test
class A(b: String @@ Test)
trait Module {
  val string: String = "".taggedWith[Test] // type here should be string: String @@ Test
  val a: A = jam.brew[A]
}

Expected: Something like No injection candidates found for (A).b
Actual:

[Error] type mismatch;
[Error]  found   : Module.this.string.type (with underlying type String)
[Error]  required: String @@ Test
[Error]     (which expands to)  String with com.softwaremill.tagging.Tag[Test]

It seems like the the macro finds and uses string (even though it's not the right type) and then the compiler itself throws an error. I would expect the macro to not use string because it's not typed correctly

Brewing from case classes creates ambiguity

trait Config

object defaultConfig extends Config

class Service(cfg:Config)

case class A(some:Config = defaultConfig)

def brew(a:A):Service =
  jam.brewFromRec[Service](a)

Compilation fails with :

More than one injection candidate was found for Config:
A._1(Config), 
A.some(Config),
A.copy$default$1(Config @uncheckedVariance)

The report is perfectly correct.
Being able to use case classes as carriers would be nice though.

Doesn't inject non-implicit parameters that are implicit fields (scala 2)

class A(b: Long, implicit val c: Int)
trait Module {
  val b: Long = ???
  val c: Int = ???
  val a: A = jam.brew[A]
}

Note that c here is in the normal parameter list, so must be provided normally to the class when constructed, but is used as an implicit field inside the class. This is effectively equivalent to

class A(b: String, _c: Int) {
  implicit val c: Int = _c
}

Expected: This works
Actual: Unable to resolve implicit instance for (A).c

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.