Skip to content

0.3.0 - Compile-time DI, mocking, optics & OpenAPI-jsoniter modules, cats-tagless derivation, built on Hearth 0.4.0

Latest

Choose a tag to compare

@MateuszKubuszok MateuszKubuszok released this 26 Jun 18:04
· 2 commits to master since this release

Four 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.derived

New 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 | Int derives as an SCoproduct (#124)
  • Config-preference selection via scalac setting (-Xmacro-settings:tapirSchemaDerivation.preferConfig=circe) and freshConfigType to avoid stale caching across ServiceLoader expansions (#124)

New in kindlings-avro-derivation

  • @avroNamespace on 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's MIO error channel and per-module typed error ADTs, deduplicated the per-module AnnotationSupport into 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
  • CollectionBuildException made public (#133, fixes #132)
  • di wiredInModule + 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!