v0.4.7 (26 June 2026)

v0.4.7 on GitHub

@HkjHttpClient: generated Effect-Path HTTP clients

hkj-spring was server-side only: the *PathReturnValueHandlers map an EitherPath/VTaskPath/… returned from a controller into an HTTP response, but when service A called service B, B's typed error collapsed into a raw status code at A's boundary, losing the typed error channel the library is built around. The new @HkjHttpClient closes that gap with the client-side inverse, preserving the typed error end-to-end across services. Two additive modules (hkj-spring/client runtime + hkj-spring/client-processor), no breaking changes.

  • Runtime (hkj-spring/client): HkjClientExchange folds an HTTP exchange into an Effect Path: either (2xx→Right, 4xx/5xx→Left), eitherVTask (deferred on a virtual thread, so callers get withRetry/withCircuitBreaker/timeout), and maybe (404/empty→Nothing). A pluggable ResponseErrorDecoder decodes the server's {"success":false,"error":…} envelope into the declared error type via the shared Jackson mapper; auto-configuration contributes the default factory.
  • Codegen (hkj-spring/client-processor): annotating a Path-typed @HttpExchange interface generates a native @HttpExchange interface (return types unwrapped to ResponseEntity<T>, all mapping/parameter annotations copied through), a …Client implementation that dispatches by return type, and a …ClientConfiguration that wires the client via Spring 7 @ImportHttpServices; base URL/timeouts/versioning come from spring.http.serviceclient.<group>.*.
  • A concrete error type decodes with no extra annotations; a sealed DomainError hierarchy needs @JsonTypeInfo/@JsonSubTypes. The processor is wired into hkj-spring-boot-starter; the hkj-spring/client-example module is a standalone client application that calls the server example over HTTP (with an end-to-end MockRestServiceServer test), and the Declarative HTTP Clients guide walks through it.
  • Additional capabilities: @OnStatus(value, error) maps individual statuses to distinct error subtypes (404 → UserNotFoundError, …); generic @HkjHttpClient interfaces are supported codegen-only; ClientErrorResponse.retryAfter() exposes the server's Retry-After hint for back-off; and HkjClientExchange.vstream(...) consumes the server's SSE stream into a VStreamPath<T> (deferred, resource-safe). The runtime itself is written in the library's own idioms (Try/Either), and the client is documented across the hkj-spring module docs plus a dedicated HTTP_CLIENT.md.

Spring Boot 4.1.0 / Framework 7.0.8 upgrade

The hkj-spring modules move from Spring Boot 4.0.6 to 4.1.0 (Spring Framework 7.0.8), with the managed Jackson 3.x line advancing from 3.1.2 to 3.1.4 to match the jackson-bom shipped by Boot 4.1.0. Dependency-only, centralised in the version catalog; the modules compile and pass against 4.1.0 with no source changes and no new deprecations. No public API change (#575).

Writer.of(log, value) factory

New Writer.of(W log, @Nullable A value) static factory for the common custom-log-plus-value case, sitting between Writer.value(Monoid<W>, A) (empty log) and Writer.tell(W) (Unit value). The (log, value) order mirrors the record components and accessors, and returning the plain Writer<W, A> launders the @Nullable A nullness contract that the raw constructor's diamond leaks at the call site. Purely additive (#554).

Consistent recoverWith / recover null-handling across MonadError

recoverWith(ma, fallback) now rejects a null ma/fallback eagerly and identically on every MonadError instance (TryMonad, OptionalMonad, VTask, CompletableFuture, EitherT/MaybeT/OptionalT), replacing the previous state-dependent, mislabelled NullPointerException (EitherMonad/ValidatedMonad already guarded it). recover(ma, value) keeps its @Nullable value, so recover(failure, null) stays a valid Success(null)/Nothing/empty; ValidatedMonad keeps only recoverWith because its of rejects null. Behaviour-preserving except on null input (#553).

Internal: type-safety and soundness cleanups

A sweep across hkj-core removing avoidable unchecked casts and holder indirection; behaviour-preserving with no public API change unless noted:

  • Turned on -Xlint:unchecked,rawtypes -Werror across all modules, so any new unchecked or raw-type use must carry an explicit suppression; generated @ComposeEffects Support classes carry one so downstream lint-enabled builds stay clean (#560).
  • Maybe/Either/Validated roots now extend their Kind interfaces, making the five widen/widen2 methods cast-free upcasts (#561).
  • Every remaining HKJ-owned type direct-implements its Kind: widen is an allocation-free upcast, seventeen *Holder records are deleted, and narrow(null) now uniformly raises KindUnwrapException (Lazy and the JDK-wrapped types keep their holders) (#568).
  • Funnelled the covariant flatMap/recoverWith reinterpretations through a private covary helper, and replaced the public API's last raw-Kind wrapper (IndexedTraversal.asIndexedFold()) with a typed IdBox (#562).
  • Consolidated Free.foldMap's two stack-safe interpreters behind the single Natural path (#563).

EachIndexed: type-safe replacement for Each.eachWithIndex()

Each.eachWithIndex() returned Optional<IndexedTraversal<I, S, A>> with a caller-chosen index type, so requesting the wrong index compiled and then failed at runtime with a ClassCastException. New EachIndexed<I, S, A> extends Each<S, A> carries the real index type at the type level and exposes indexedTraversal() directly (no Optional, no cast); the EachInstances factories now return it. Each.eachWithIndex() is deprecated for removal in 0.5.0 and still works as a bridge, so existing code compiles. Additive plus one deprecation, no behaviour change for existing callers (#564). See Each type class / Indexed Optics.

raw-kind checker rule

The HKJ compiler plugin gains a raw-kind rule: a raw Kind/Kind2 drops its witness type argument (the one route that lets a value tagged with one witness be narrowed through another, compiling silently and throwing KindUnwrapException at runtime), and javac accepts it, so the checker is the sole compile-time signal. Flags variable/parameter/field declarations and casts at warn by default (disable=raw-kind, severity:raw-kind=error); a properly parameterised Kind<W, A> is never flagged (#565). Documented in Compile-Time Checks.


Previous: v0.4.8 Next: v0.4.6