Skip to content

Releases: floatdrop/di

v0.14.0

Choose a tag to compare

@floatdrop floatdrop released this 07 Sep 14:44
d1e5c63

A module dependency report, derived from what registrations already carry.
go doc -all against 0.13.1 adds Scope.Modules and changes no signature;
an upgrade cannot break a caller.

Added

  • Scope.Modules() renders the modules registered into a scope and its
    ancestors: what each provides, what it needs and which module serves it,
    what it wraps, and which of its constructors are closures whose needs are
    known only when they run. A need only a resolving scope can provide is
    reported as owed, as Validate reports it, and a module's dependency on its
    own services is left out. It is derived from what registrations already
    carry, the module label and the parameters Wire declares, so there is
    nothing to declare twice. Keys are named by their package rather than their
    import path, to read beside the module labels.

v0.13.1

Choose a tag to compare

@floatdrop floatdrop released this 07 Sep 13:00
c444499

Three defects reported in #35, each with a reproduction, each real. go doc -all against 0.13.0 is unchanged in di and dihttp; an upgrade cannot
break a caller, and every fix turns a panic, a false rejection or a lost
error into the behaviour the documentation already promised.

Fixed

  • Wire and Wrap accepted a result merely assignable to the key, such as a
    chan int for a <-chan int key or a []byte for a named slice type, and
    then stored it as the constructor's own type, so Get panicked asserting
    it. The value is stored as the key's type now (#35).
  • Validate reported a cycle where a valid graph visited one Scoped
    binding in two scopes. A node on its path is now a binding in a holder, as
    it is on a resolution path at run time, so the two instances are told
    apart (#35).
  • A Worker that returned its own failure joined with the cancellation, as
    errors.Join(ctx.Err(), err), had the failure dropped with the
    cancellation, and Stop returned nil. Only an error that says nothing
    beyond the cancellation is dropped now (#35).

v0.13.0

Choose a tag to compare

@floatdrop floatdrop released this 06 Sep 17:50
7864772

One route, one line. go doc -all against 0.12.0 leaves di untouched and
adds dihttp.Handle; nothing changes shape or behaviour, and an upgrade
cannot break a caller.

Added

  • dihttp.Handle(method) makes an http.Handler that resolves a handler
    type from the request's scope and calls one of its methods, named by a
    method expression: mux.Handle("GET /users/{id}", dihttp.Handle((*Users).Show)).
    One type per resource with a method per route replaces a type, a
    registration and a closure per route. The type is Scoped when it needs
    the request and a plain singleton when it does not; Handle follows
    either. Nothing existing changes; an upgrade cannot break a caller.

v0.12.0

Choose a tag to compare

@floatdrop floatdrop released this 06 Sep 16:16
529bff2

The request-scope middleware becomes a dependency a server's constructor can
take, so an application needs no Provide closure to serve HTTP. go doc -all against 0.11.0 leaves di untouched and changes dihttp: Middleware
is now the type, NewMiddleware makes one, Module registers one. The rename
is the breaking change, and the reason for the minor bump.

Changed

  • dihttp.Middleware is now the type, func(http.Handler) http.Handler, so
    a server's constructor can take one as a parameter; the function that
    makes one is dihttp.NewMiddleware. A call dihttp.Middleware(app) no
    longer compiles: rename it, or take the middleware as a dependency.

Added

  • dihttp.Module registers a Middleware over the scope it is applied to.
    With it the guide's server is a wired constructor with its dependencies
    declared, and the guide application uses no Provide closure at all.

v0.11.0

Choose a tag to compare

@floatdrop floatdrop released this 06 Sep 15:50
3d6c4df

Validate no longer needs an adapter to check request scopes: the caller
says what such a scope will hold. go doc -all against 0.10.0 adds Stub
and Provided, gives Scope.Validate a variadic parameter, which every
existing call satisfies, and removes dihttp.Validate, the one breaking
change.

Added

  • di.Provided[T]() makes a Stub, a key the scope resolving a Scoped
    binding will provide. s.Validate(stubs...) then makes the check that scope
    would make: what the stubs cover is satisfied, and what neither the scope
    nor the stubs provide is an error rather than Owed. Stubs apply to the
    Scoped path only; a singleton that would build a Scoped service in its
    own scope still fails there, whatever a request scope holds.

Removed

  • dihttp.Validate, which opened a throwaway request scope to do what
    app.Validate(di.Provided[*http.Request]()) now does from the application
    scope, without an HTTP-shaped helper in the way. Replace the call with that
    expression and .Err().

v0.10.0

Choose a tag to compare

@floatdrop floatdrop released this 06 Sep 08:21
e4999c2

A service can be wrapped without being replaced (#1), which was the last
open issue and the gap most often felt next to uber/fx. go doc -all
against 0.9.1 adds Scope.Wrap and changes no signature; an upgrade cannot
break a caller, since every new rejection involves a wrapper.

Added

  • Scope.Wrap[T](fn) composes over whatever serves T when it is called,
    the latest registration in this scope or the one an ancestor provides
    (#1). fn takes the wrapped value first and its other dependencies after
    it, read with reflection as Wire reads a constructor, and returns T or
    (T, error). What is wrapped keeps its registration, hooks and lifetime, is
    built first and stopped after the wrapper; wrappers chain in registration
    order; a wrapper takes the wrapped lifetime, and Scoped() on it makes one
    per resolving scope over a shared inner. A wrapper in a child scope applies
    to that scope and its descendants only, which is what uber/fx calls
    Decorate. Nothing to wrap, a group, and the Group() or Override()
    markers on a wrapper are rejected. A registration some wrapper composes
    over can no longer be overridden, in its scope or a descendant's, until an
    Override() replaces the wrapper itself. Explain names a wrapper and
    draws what it wraps beneath it; Validate walks the chain.

v0.9.1

Choose a tag to compare

@floatdrop floatdrop released this 06 Sep 07:37
d1c037a

Explain shows the graph Wire declared before it is built. go doc -all
against 0.9.0 adds no symbol and changes no signature, and an upgrade cannot
break a caller; only the rendering of an unbuilt Wire service gains lines.

Changed

  • Explain draws the dependencies a Wire binding declares when it has not
    been built: dashed edges under the node, each continuing as the recorded
    tree where the dependency has been built and as a declared one where it has
    not, ending at a closure or at a key nothing provides. A declared by: line
    names the unbuilt services that declare a key, beside needed by: for the
    built ones. The output for a built service is unchanged, and Graph still
    renders only what was built.

v0.9.0

Choose a tag to compare

@floatdrop floatdrop released this 06 Sep 06:42
e07354f

Constructors can be handed over as they are written, and the graph they
declare can be checked before anything is built (#3). Nothing existing
changes shape or behaviour; an upgrade cannot break a caller.

go doc -all against 0.8.0 adds Scope.Wire, Scope.Validate, Validation
with its Err method, and dihttp.Validate, and changes no signature. The
README no longer promises no reflection at all: Wire reads a constructor's
signature once at registration and calls it through reflect.Call; Provide
is as it was.

Added

  • Scope.Wire[T](ctor) registers a plain constructor, func(A, B) T or
    func(A, B) (T, error), whose parameters are its dependencies. Each is
    resolved as a Provide closure would resolve it, from the same scope, so
    lifetimes, hooks, cycles and error paths are unchanged. The signature is
    read with reflection once at registration, where a constructor of the wrong
    shape is rejected like any other configuration error; a result merely
    assignable to T is accepted, so a concrete constructor can serve an
    interface key. The build calls the constructor through reflect.Call, about
    150ns and two allocations over a closure. Provide is untouched.
  • Scope.Validate() walks the declared graph without building and returns a
    Validation: Errors (joined by Err()) for a dependency nothing provides,
    a cycle among Wire constructors, or a singleton that would build a Scoped
    service in a scope that cannot satisfy it; Owed for what a Scoped
    binding needs that the validating scope does not provide, since the scope
    resolving it may; Unchecked for the Provide closures.
  • dihttp.Validate(app) validates from a throwaway request scope holding an
    *http.Request, so what a request scope would still miss is an error.

v0.8.0

Choose a tag to compare

@floatdrop floatdrop released this 06 Sep 04:06
2dc9c50

v0.8.0

Two features and one rule. Explain and Graph render the dependency graph as it
was actually built. Modules compose by Use, with every registration attributed
to the module that made it. And the rule that makes modules safe to compose: a
second registration of a key within one scope must say so with Override(), or
it is rejected naming both sites. That last one is breaking, and the reason for
the minor bump.

go doc -all against v0.7.0 adds Binding.Override, Module, Scope.Use,
Scope.Explain and Scope.Graph, and changes one signature: Test takes
...Module.

Behaviour that can break an upgrade:

  • A duplicate registration of a key within one scope no longer wins silently.
    It is rejected at the next resolution, naming both registrations and, when
    they came from modules, both modules:

    di: *app.DB is provided at app.Storage (wire.go:12) and again at
    app.Caching (cache.go:8): a second registration of a key must be
    marked Override() to replace the first

    Mark the registration that means to replace the earlier one: for the test
    seam, s.Value(&fake).Override(). An Override() with nothing to override in
    its scope is rejected too, since a fake for a service that has since been
    renamed would otherwise be a registration nobody resolves. A child scope
    still shadows its parent without any marker. A key that has served a value
    still cannot be replaced at all.

  • Test takes ...Module. Function literals and named functions are assignable,
    so those calls compile unchanged; a call that spreads a []func(*Scope) with
    ... needs the slice typed []Module.

Added:

  • Scope.Explain[T] prints the dependency tree of a service as it was built,
    with each node's lifetime, scope, lifecycle state and registration site, and
    the instances that needed it. Scope.Graph exports everything built as
    Graphviz DOT, one cluster per scope. Neither builds anything. The recording
    behind them costs nothing on the warm path.

  • Module is a named func(*Scope); Scope.Use applies modules in order and
    attributes every registration they make -- directly, from a child the module
    opens, or later from a constructor the module registered -- to the module's
    function name. Event.Module carries the same name to observers.

Fixed:

  • A panicking OnDrain or OnStop hook is reported as that hook's failure rather
    than propagating out of Stop. The start step was always recovered this way;
    the other two were not, so a panic in either abandoned the teardown halfway:
    stopOnce claimed and never settled, every later Stop waiting on it until its
    context ran out, every instance behind it never released. Found by the
    concurrent fuzzer the moment it could build a scope with a permanently
    rejected registration -- a drain hook resolving through one meets the
    rejection as a panic.

Internal: the machinery the 0.7.0 API cuts left behind is gone. A resolution
path node is 32 bytes rather than 48, so a warm resolve allocates 64 B rather
than 96 B and runs about 8% faster.

Testing: the generators register through Use and carry an override bit, the
property model gained the override rule and was mutation-tested clause by
clause, and generator coverage went from 88.5% to 92.1% with the CI floor
raised to 90.

v0.7.0

Choose a tag to compare

@floatdrop floatdrop released this 05 Sep 15:14
1b91630

The API surface cut down ahead of the graph-validation work of #3, so that it lands on a smaller and more regular API, plus the four defects of the fourth September 2026 review.

This release breaks callers. Nine identifiers are gone and one is renamed; go doc -all diffed against 0.6.0 lists exactly those and one addition, Binding.Group. Every one has a one-line migration:

0.6.0 0.7.0
.Named("x"), s.Lookup(di.Named[T]("x")), Key, di.Named a distinct type: type ReplicaDB struct{ *DB }
Binding.Run(f) Binding.Worker(f)
s.Add(ctor) s.Provide(ctor).Group()
s.Bind[Reader, *Repo]() s.Provide(func(s *di.Scope) Reader { return s.Get[*Repo]() }), .Scoped() too when the target is
.Transient() a factory closure, or call the constructor
.Health(f), HealthCheck, ErrUnhealthy, EventHealth a Group of checkers; the README's Groups section is the recipe
app.Middleware(mux) dihttp.Middleware(app)(mux), from the new dihttp package
di.Signals(...) none; Run exits on os.Interrupt and SIGTERM
di.SlogObserver(l) four lines in user code

Why these: Named was the one stringly-typed corner, and a newtype is checked at every reference where a misspelt name was a missing key at runtime. Bind carried the most machinery per use in the library, and a constructor that returns the implementation is checked by the compiler where Bind checked Implements at registration. Transient had no hooks and no tracking, a lifetime in name only, and every teardown oracle carried an exemption for it; the singleton-stability invariant now has none. Health checks are an application concern that a group expresses in three lines. The core package no longer imports net/http or log/slog, and registers nothing on a caller's behalf.

The rest is correctness, from the fourth review. Scope.Run reports a cause published through Shutdown while a failed Start was rolling back, not only during an ordinary stop. A Stop reports the failure of a drain hook of its own scope even when an ancestor's Stop ran the hook, which is the request-scope-during-shutdown case. And a key can no longer be overridden while a resolution of it is in flight, which closed a path to two live values for one key from a single goroutine.

Testing: drain hooks can fail in the drivers, the machines drive Scope.Run, and two registration shapes have constructors that register. Generator coverage went from 82.5% to 90.2%, and CI now fails below 85%. The coverage-gap script itself turned out to be wrong, merging blocks that shared a line, and CI now checks its arithmetic against go tool cover.

Full notes with the reasoning behind each cut are in CHANGELOG.md.