Skip to content

DI for Cats-Effect Resource

kindlings-di-cats adds one macro on top of the DI module: DICats.wireResource, which wires a value whose dependencies are Cats-Effect Resources (or effects, or plain values) and hands you back a single composed Resource.

It is the Kindlings answer to macwire's autowire, with one important difference: it is not tied to any particular effect type. macwire's autowire always returns Resource[IO, _]; wireResource is parametric in F[_], so it works for IO, SyncIO, or any other effect — and needs no Sync/Async/MonadCancel constraint, because Resource's pure/eval/flatMap require none.

Installation

sbt

libraryDependencies += "com.kubuszok" %%% "kindlings-di-cats" % "0.3.1"

Scala CLI

//> using dep com.kubuszok::kindlings-di-cats:0.3.1

kindlings-di-cats is published for the JVM and Scala.js. (Scala Native support follows the Cats-Effect Native artifact, first published from cats-effect 3.7.0.)

Quick start

Give wireResource the dependencies it should compose; it resolves each parameter of the target's constructor from them and threads the Resource plumbing for you:

//> using dep com.kubuszok::kindlings-di-cats:0.3.1
//> using dep org.typelevel::cats-effect:3.6.3

import hearth.kindlings.dicats.DICats
import cats.effect.{Resource, SyncIO}

class Db
class Service(db: Db)

val dbResource: Resource[SyncIO, Db] = Resource.eval(SyncIO(new Db))

// db is taken from the Resource and threaded through; the result is a single Resource[SyncIO, Service]:
val app: Resource[SyncIO, Service] = DICats.wireResource[SyncIO, Service](dbResource)

println(app.use(service => SyncIO(service.getClass.getSimpleName)).unsafeRunSync())
// expected output:
// Service

What can be a dependency

Each argument to wireResource is classified by its type:

Dependency type How it is used
Resource[F, X] acquired with flatMap; its X is available to the construction
F[X] (an effect) lifted with Resource.eval, then flatMaped
plain X used directly — spliced into the construction with no Resource wrapping

The target itself is built from its public primary constructor (or, failing that, a single matching companion apply, or a companion resource factory — see below). Acquisition happens in the order the dependencies are given and release happens in reverse, exactly as if you had written the nested flatMaps by hand.

import cats.effect.{Resource, SyncIO}

class Config
class Db
class Cache
class App(config: Config, db: Db, cache: Cache)

val config = new Config                                     // plain value
val db: Resource[SyncIO, Db] = Resource.eval(SyncIO(new Db))
val cache: SyncIO[Cache] = SyncIO(new Cache)                // effect

val app: Resource[SyncIO, App] = DICats.wireResource[SyncIO, App](config, db, cache)

Companion resource[F] factories

A very common pattern is for a component to expose its own lifecycle via a companion method def resource[F[_]: <constraints>]: Resource[F, T]. wireResource discovers and uses it automatically — when it needs a T that no dependency provides, and T's companion has a resource method, it applies your F and lets the compiler resolve the context-bound implicits (e.g. Sync[F]). You don't have to pass such a component in yourself:

import cats.effect.{Resource, Sync, SyncIO}

class Connection
object Connection {
  // acquisition/release is defined here; `F` is chosen by whoever calls `wireResource`
  def resource[F[_]: Sync]: Resource[F, Connection] =
    Resource.make(Sync[F].delay(new Connection))(_ => Sync[F].unit)
}

class Service(connection: Connection)

// Connection is built via `Connection.resource[SyncIO]` — no need to provide it explicitly
val service: Resource[SyncIO, Service] = DICats.wireResource[SyncIO, Service]()

This is a Kindlings extension over macwire's autocats (it has no equivalent). Both constraint-free (def resource[F[_]]) and context-bounded (def resource[F[_]: Sync]) shapes are supported; a resource method that takes explicit value parameters is left to ordinary construction.

Debugging

di-cats has the same debugging capabilities as kindlings-di:

  • Generation logging — import hearth.kindlings.dicats.debug._ (or set -Xmacro-settings:diCats.logGeneration=true) to print the resolution logic, the wiring graph, and the generated Resource code.
  • Wiring graph — set -Xmacro-settings:diCats.logWiring=tree (or =mermaid) to see only how the resources combine, as a ZIO-Magic-style dependency tree (or Mermaid diagram). Provided instances, Resource/effect dependencies, factories, and resource[F] factories are each labelled in the graph.

See Debugging & Diagnostics.

Errors

wireResource mirrors macwire's diagnostics. If no dependency provides a constructor parameter you get a "Cannot find a value of type …" compile error; if two dependencies could provide the same parameter you get an "Ambiguous …" error.

Relationship to kindlings-di

Use kindlings-di (wire, wireSet, …) when you wire plain values from the enclosing lexical scope. Reach for kindlings-di-cats when the components are lifecycle-managed Resources that must be acquired and released together.