0.3.0 - Compile-time DI, mocking, optics & OpenAPI-jsoniter modules, cats-tagless derivation, built on Hearth 0.4.0
LatestFour brand-new Hearth-based modules — compile-time dependency injection (macwire-style), mocking (ScalaMock-style), optics (quicklens-style), and a Circe-free OpenAPI jsoniter serializer — plus a new cats-tagless derivation module, Scala 3 union-type Tapir schemas, more jsoniter/Avro features, and a migration to sbt 2.0. All built on Hearth 0.4.0.
Where 0.2.0 grew the derivation catalogue, 0.3.0 broadens what "Hearth-powered, cross-compiled to JVM / Scala.js / Scala Native on Scala 2.13 and 3" means: the same macro engine now also gives you DI, mocking and optics — each an independent, from-scratch reimplementation of a popular SoftwareMill/ScalaMock library, with no reflection and no bytecode generation.
⚠️ Build: now on sbt 2.0 and Hearth 0.4.0
Kindlings is built with sbt 2.0 (#149) and the Hearth dependency is bumped to 0.4.0 (#124, #150). This is a build/internal change — the derivation API is source-compatible with 0.2.0 — but if you depend on Kindlings SNAPSHOTs or build from source, note the new toolchain. The 0.4.0 migration also moved every module onto Hearth's new method-builder chain (Method.fold, totalParameters, default-value extraction) and AnonymousInstance for trait derivation.
New module: kindlings-di (compile-time DI, macwire-style)
A macwire port built on Hearth's enclosingScope. wire[A] inspects the enclosing lexical scope at compile time and emits the constructor call — no reflection, no runtime container, and (unlike macwire) the same code cross-compiles to JVM, Scala.js and Scala Native on 2.13 and 3 (#137, #140):
import hearth.kindlings.di.DI
class DatabaseAccess()
class SecurityFilter()
class UserFinder(databaseAccess: DatabaseAccess, securityFilter: SecurityFilter)
class UserModule {
lazy val databaseAccess = new DatabaseAccess()
lazy val securityFilter = new SecurityFilter()
lazy val userFinder: UserFinder = DI.wire[UserFinder] // = new UserFinder(databaseAccess, securityFilter)
}The full macwire surface is covered: bare wire / wireSet / wireList / wireWith / wireRec / autowire, @@ tagging (the SoftwareMill tagging library is pulled in transitively), @Module composition, nearest-scope precedence, and macwire-style ambiguity / missing-dependency path diagnostics (wiring path: A -> B -> A).
New module: kindlings-di-cats (Cats-Effect Resource wiring)
One macro on top of kindlings-di: DICats.wireResource, the Kindlings answer to macwire's autowire — but not tied to any effect type. macwire's autowire always returns Resource[IO, _]; wireResource is parametric in F[_] and needs no Sync/Async/MonadCancel constraint (#137):
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))
val app: Resource[SyncIO, Service] = DICats.wireResource[SyncIO, Service](dbResource)Each argument is classified by type: Resource[F, X] is acquired with flatMap, an effect F[X] is lifted with Resource.eval, and a plain X is spliced in directly. Published for JVM and Scala.js.
New module: kindlings-mock (ScalaMock-style mocking)
An independent, from-scratch reimplementation of the ScalaMock API (it does not depend on or wrap ScalaMock). Mock.mock / Mock.stub synthesize an anonymous subtype via Hearth's AnonymousInstance and route every member into a tiny pure-Scala engine — no reflection, no bytecode — so the same tests run on JVM, Scala.js and Scala Native (#137):
import hearth.kindlings.mock._
import hearth.kindlings.mock.syntax._
trait Greeter {
def greet(name: String): String
}
implicit val ctx: MockContext = new MockContext
val greeter = Mock.mock[Greeter]
// faithful ScalaMock DSL — eta-expand the method, then `.expects`:
val _ = (greeter.greet _).expects("world").returning("hello, world")
val greeting = greeter.greet("world")
ctx.verifyExpectations()Both the faithful (m.method _).expects(...).returning(...) DSL and a name-keyed ctx.expecting("method", args) DSL register into the same MockContext. Includes returning/throwing/onCall, call-count ranges, inSequence/inAnyOrder ordering, and any/where/argThat/epsilon/capture matchers.
New module: kindlings-optics (quicklens-style lenses)
An independent reimplementation of SoftwareMill quicklens. obj.modify(_.a.b.c) parses the path lambda at compile time and emits a nested copy-with-modification — no reflection, no bytecode, all platforms (#140):
import hearth.kindlings.optics._
final case class Street(name: String)
final case class Address(street: Street)
final case class Person(name: String, address: Address)
val p = Person("Anna", Address(Street("Main")))
val updated = p.modify(_.address.street.name).using(_.toUpperCase)
// updated.address.street.name == "MAIN"Terminals include using/setTo/setToIfDefined/setToIf/usingIf, and .each / .eachWhere(cond) traversals over collections, Options and Maps — resolved through Hearth's IsCollection/IsMap/IsOption SPI, so any container a provider registers (including Cats non-empty collections) works.
New module: kindlings-tapir-openapi-jsoniter (Circe-free OpenAPI serialization)
Serialize the OpenAPI document that tapir generates straight to JSON with jsoniter-scala — without pulling in circe-core and half of Cats just to render a static document (#137). Hand-written, byte-for-byte-compatible (verified in CI) jsoniter codecs for the whole sttp-apispec OpenAPI + JSON-Schema model, plus a thin TapirOpenApi bridge:
import hearth.kindlings.tapiropenapijsoniter._
import sttp.apispec.openapi.Info
import sttp.tapir._
val health = endpoint.get.in("health").out(stringBody)
// endpoints -> OpenAPI document -> JSON, serialized by jsoniter (not Circe):
val json: String = TapirOpenApi.endpointToJson(health, Info("Health API", "1.0"))The codecs (OpenApiJsoniter.openapi_3_1._ / OpenAPI 3.0 variant) are cross-platform including Native; the tapir bridge follows tapir-openapi-docs (JVM + Scala.js).
New module: kindlings-cats-tagless-derivation
A drop-in replacement for cats-tagless's Derive.* / derived macros — derives FunctorK, ContravariantK, InvariantK, ApplyK, SemigroupalK and Instrument for tagless-final algebras (both trait Alg[F[_]] and case class Alg[F[_]]), on Scala 2.13 and Scala 3, variance-aware (#124):
import hearth.kindlings.catstaglessderivation._
import cats.tagless.FunctorK
trait UserRepo[F[_]] {
def find(id: Int): F[String]
}
val functorK: FunctorK[UserRepo] = KindlingsFunctorK.derivedNew in kindlings-jsoniter-derivation
Three new config options (#124): inlineOneValueClasses, skipNestedOptionValues and alwaysEmitDiscriminator — each with a runtime-config fallback when compile-time semiEval is unavailable.
New in kindlings-tapir-schema-derivation
- Scala 3 union-type schemas:
String | Intderives as anSCoproduct(#124) - Config-preference selection via scalac setting (
-Xmacro-settings:tapirSchemaDerivation.preferConfig=circe) andfreshConfigTypeto avoid stale caching across ServiceLoader expansions (#124)
New in kindlings-avro-derivation
@avroNamespaceon fields, with namespace-override propagation (#124)
Runtime & macro-compile performance
A further round of runtime and macro-compilation optimizations and an API rename landed across the derivation modules, with refreshed benchmark numbers (#141).
Hardening & internals
- Eliminated macro-time
throws across nine modules in favour of Hearth'sMIOerror channel and per-module typed error ADTs, deduplicated the per-moduleAnnotationSupportinto one shared implementation (~800 lines removed), and documented/closed Hearth API gaps (#129) — the workarounds for hearth#283/#284/#285 are now resolved and migrated off CollectionBuildExceptionmade public (#133, fixes #132)diwiredInModule+autowireMembersOf, plus extensive macwire/ScalaMock-parity test suites for the new modules (#140)
Documentation
Every new module ships a user guide on kindlings.readthedocs.io with runnable, CI-verified Scala CLI snippets: DI (macwire), DI for Cats-Effect, Mocking, Optics, Cats Tagless, and Tapir OpenAPI Jsoniter.
Dependency updates
Hearth 0.4.0, Scala 3.8.4, Scala 2.13.18, tapir 1.13.23, circe 0.14.16, jsoniter-scala-circe 2.38.16, cats-effect 3.7.0, scala-yaml 0.3.2, sconfig 2.0.0, quicklens 1.9.15, and CI action bumps (#126, #130, #131, #135, #136, #139, #143, #146, #147, #148, #152, #154, #155).
Summary
0.3.0 takes Kindlings beyond derivation: the same Hearth macro engine now powers compile-time DI, mocking and optics, a Circe-free OpenAPI serializer, and cats-tagless derivation — all cross-compiled to JVM, Scala.js and Scala Native, all documented with verified examples.
Be sure to look at the documentation, star the project ⭐ and leave us some feedback!