Documentation
¶
Overview ¶
Package gold is a generics-first dependency layer over grr, the untyped name-keyed lifecycle registry. It keeps the definition of a dependency separate from its implementation and makes that separation type-safe, with no reflection anywhere.
It offers two typed primitives over the same grr storage:
Port[T] — a named, typed, lifecycle-managed binding to any T (an interface, a struct, a pointer). Use it for ordinary collaborator and resource dependencies: a config, a *sql.DB, an HTTP client, or a request-scoped data object with several accessor methods. Resolve(ctx) hands back a typed T.
Logic[request, response, model, impl] — a specialization of Port whose T is a unit of domain behaviour invoked as Do(ctx, request) (response, error). Use it for the hexagonal boundary between domain logic and its implementation, where the call site (Logic.Do) should depend only on the typed contract, never on what's behind it.
Both share the same lifetime sugar (RegisterSingleton/Scoped/Transient) and the same startup check: every Port and Logic declared with NewPort/NewLogic is verified to have a registered implementation by Validate/MustValidate.
gold is not a general-purpose IoC container: no struct-tag injection, no automatic dependency-graph resolution, no reflection. For wiring an entire application's dependency graph automatically, wire, dig, or fx are a better fit. gold does one thing — gives a name and a compile-time-checked type to a dependency and lets its implementation be wired separately.
The single place gold crosses from typed code into grr's untyped storage is one type assertion per resolve (raw.(T)) — a plain Go assertion, not reflect.
Index ¶
- func LogicFunc[request, response any](fn func(context.Context, request) (response, error)) func(struct{}) funcImpl[request, response]
- func MustValidate(r *grr.Registry)
- func RegisterStateless[request, response any, impl Logical[request, response]](l *Logic[request, response, struct{}, impl])
- func RegisterStatelessIn[request, response any, impl Logical[request, response]](r *grr.Registry, l *Logic[request, response, struct{}, impl])
- func Validate(r *grr.Registry) error
- func ValidateLogics(r *grr.Registry, names ...string) error
- type Logic
- func (l *Logic[request, response, model, impl]) Do(ctx context.Context, req request) (response, error)
- func (l *Logic[request, response, model, impl]) IsRegistered(ctx context.Context) bool
- func (l *Logic[request, response, model, impl]) Register(factory func(ctx context.Context) impl)
- func (l *Logic[request, response, model, impl]) RegisterIn(r *grr.Registry, factory func(ctx context.Context) impl)
- func (l *Logic[request, response, model, impl]) RegisterScoped(newModel func(ctx context.Context) model)
- func (l *Logic[request, response, model, impl]) RegisterScopedIn(r *grr.Registry, newModel func(ctx context.Context) model)
- func (l *Logic[request, response, model, impl]) RegisterSingleton(newModel func() model)
- func (l *Logic[request, response, model, impl]) RegisterSingletonIn(r *grr.Registry, newModel func() model)
- func (l *Logic[request, response, model, impl]) RegisterTransient(newModel func(ctx context.Context) model)
- func (l *Logic[request, response, model, impl]) RegisterTransientIn(r *grr.Registry, newModel func(ctx context.Context) model)
- func (l *Logic[request, response, model, impl]) TryDo(ctx context.Context, req request) (resp response, ok bool, err error)
- type Logical
- type MissingError
- type Port
- func (p *Port[T]) IsRegistered(ctx context.Context) bool
- func (p *Port[T]) Register(factory func(ctx context.Context) T)
- func (p *Port[T]) RegisterIn(r *grr.Registry, factory func(ctx context.Context) T)
- func (p *Port[T]) RegisterScoped(newVal func(ctx context.Context) T)
- func (p *Port[T]) RegisterScopedIn(r *grr.Registry, newVal func(ctx context.Context) T)
- func (p *Port[T]) RegisterSingleton(newVal func() T)
- func (p *Port[T]) RegisterSingletonIn(r *grr.Registry, newVal func() T)
- func (p *Port[T]) RegisterTransient(newVal func(ctx context.Context) T)
- func (p *Port[T]) RegisterTransientIn(r *grr.Registry, newVal func(ctx context.Context) T)
- func (p *Port[T]) Resolve(ctx context.Context) T
- func (p *Port[T]) TryResolve(ctx context.Context) (T, bool)
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func LogicFunc ¶
func LogicFunc[request, response any](fn func(context.Context, request) (response, error)) func(struct{}) funcImpl[request, response]
LogicFunc adapts fn into a newLogic-compatible constructor for stateless logic that needs no model struct:
var Ping = gold.NewLogic("Ping", gold.LogicFunc(
func(ctx context.Context, req PingRequest) (*PingResponse, error) {
return &PingResponse{Message: "pong"}, nil
},
))
Register it with RegisterStateless (no model factory needed).
Example ¶
LogicFunc lets you write stateless logic as a plain function — no model struct needed. Register it with RegisterStatelessIn (no factory needed).
package main
import (
"context"
"fmt"
"github.com/arpaad/grr"
gold "github.com/arpaad/gold"
)
type greetRequest string
type greetResponse struct{ Text string }
func main() {
r := grr.New()
Ping := gold.NewLogic("Ping", gold.LogicFunc(
func(_ context.Context, req greetRequest) (*greetResponse, error) {
return &greetResponse{Text: "pong: " + string(req)}, nil
},
))
gold.RegisterStatelessIn(r, Ping)
ctx := grr.WithRegistry(context.Background(), r)
resp, _ := Ping.Do(ctx, greetRequest("hello"))
fmt.Println(resp.Text)
}
Output: pong: hello
func MustValidate ¶
MustValidate is Validate but panics on a non-nil error — for use in main() at startup, where an unwired logic should stop the process before it serves traffic.
func RegisterStateless ¶
func RegisterStateless[request, response any, impl Logical[request, response]](l *Logic[request, response, struct{}, impl])
RegisterStateless is sugar for RegisterStatelessIn(grr.Default, l).
func RegisterStatelessIn ¶
func RegisterStatelessIn[request, response any, impl Logical[request, response]](r *grr.Registry, l *Logic[request, response, struct{}, impl])
RegisterStatelessIn registers a logic built with LogicFunc — no model factory needed since LogicFunc's model is always struct{}. Only compiles for logics shaped that way, enforced by the type signature.
func Validate ¶
Validate checks that every element (logic or port) declared with NewLogic/NewPort has an implementation registered in r (parent chain included), returning a *MissingError naming any that don't, or nil if all are wired.
Call it in main(), after all init()/wiring has run, to turn the blank-import footgun (a forgotten `import _ "myapp/di"`) into a fail-fast startup check instead of a panic on the first request that hits an unwired logic or port.
Validate assumes a single app-wide registry chain. For multi-registry setups (e.g. per-tenant registries that each wire a different subset), use ValidateLogics with the explicit set instead.
func ValidateLogics ¶
ValidateLogics checks the given element names (logics or ports) against r without consulting the global declared set — use it for isolated tests, or for multi-registry setups where each registry wires a different subset.
Types ¶
type Logic ¶
type Logic[request, response, model any, impl Logical[request, response]] struct { // contains filtered or unexported fields }
Logic is a typed business logic unit: it carries a name and a constructor (model -> impl). It is a specialization of Port[impl] — a port whose value type has a Do method — plus the model→impl construction sugar. The actual implementation instance lives in a grr.Registry, wired up separately from the definition.
All 4 type parameters are normally inferred from the newImpl constructor passed to NewLogic — they don't need to be written out, but LSP hover still shows the full type, which doubles as documentation.
func NewLogic ¶
func NewLogic[request, response, model any, impl Logical[request, response]](name string, newImpl func(model) impl) *Logic[request, response, model, impl]
NewLogic defines a logic unit. It does not register anything — wiring happens separately via Register/RegisterIn (and friends).
Example ¶
NewLogic defines the logic unit without wiring it anywhere. Wiring happens separately — here via RegisterSingletonIn.
package main
import (
"context"
"fmt"
"github.com/arpaad/grr"
gold "github.com/arpaad/gold"
)
type greetRequest string
type greetResponse struct{ Text string }
type greeter interface {
Greet() string
}
type greetLogic struct{ greeter }
func newGreet(m greeter) *greetLogic { return &greetLogic{greeter: m} }
func (l *greetLogic) Do(_ context.Context, req greetRequest) (*greetResponse, error) {
return &greetResponse{Text: l.Greet() + string(req)}, nil
}
type staticGreeter struct{ msg string }
func (s *staticGreeter) Greet() string { return s.msg }
func main() {
r := grr.New()
Greet := gold.NewLogic("Greet", newGreet)
Greet.RegisterSingletonIn(r, func() greeter {
return &staticGreeter{msg: "Hello, "}
})
ctx := grr.WithRegistry(context.Background(), r)
resp, _ := Greet.Do(ctx, greetRequest("world"))
fmt.Println(resp.Text)
}
Output: Hello, world
func (*Logic[request, response, model, impl]) Do ¶
func (l *Logic[request, response, model, impl]) Do(ctx context.Context, req request) (response, error)
Do resolves the registered implementation from ctx's registry (falling back to grr.Default) and runs it. Panics if no implementation is registered for this logic — that's a developer error, not a runtime condition callers should branch on (use IsRegistered to check first if registration is conditional).
func (*Logic[request, response, model, impl]) IsRegistered ¶
IsRegistered reports whether an implementation is registered for this logic in ctx's registry (or its parent chain).
Example ¶
IsRegistered lets call sites check registration before calling Do — useful for optional, feature-flagged logic that may not be wired in all deployments.
package main
import (
"context"
"fmt"
"github.com/arpaad/grr"
gold "github.com/arpaad/gold"
)
type greetRequest string
type greetResponse struct{ Text string }
type greeter interface {
Greet() string
}
type greetLogic struct{ greeter }
func newGreet(m greeter) *greetLogic { return &greetLogic{greeter: m} }
func (l *greetLogic) Do(_ context.Context, req greetRequest) (*greetResponse, error) {
return &greetResponse{Text: l.Greet() + string(req)}, nil
}
type staticGreeter struct{ msg string }
func (s *staticGreeter) Greet() string { return s.msg }
func main() {
r := grr.New()
Greet := gold.NewLogic("Greet", newGreet)
ctx := grr.WithRegistry(context.Background(), r)
fmt.Println(Greet.IsRegistered(ctx))
Greet.RegisterSingletonIn(r, func() greeter { return &staticGreeter{msg: "Hi"} })
fmt.Println(Greet.IsRegistered(ctx))
}
Output: false true
func (*Logic[request, response, model, impl]) Register ¶
Register is sugar for RegisterIn(grr.Default, factory).
func (*Logic[request, response, model, impl]) RegisterIn ¶
func (l *Logic[request, response, model, impl]) RegisterIn(r *grr.Registry, factory func(ctx context.Context) impl)
RegisterIn is the base registration primitive — full freedom over how impl is produced per resolve (external cache, A/B testing, tenant routing, etc). Register* sugar below all reduce to this.
func (*Logic[request, response, model, impl]) RegisterScoped ¶
func (l *Logic[request, response, model, impl]) RegisterScoped(newModel func(ctx context.Context) model)
RegisterScoped is sugar for RegisterScopedIn(grr.Default, newModel).
func (*Logic[request, response, model, impl]) RegisterScopedIn ¶
func (l *Logic[request, response, model, impl]) RegisterScopedIn(r *grr.Registry, newModel func(ctx context.Context) model)
RegisterScopedIn builds the model (and impl) once per active scope (see grr.Registry.BeginScope) and reuses it for the rest of that scope. Resolving without an active scope panics — never register scoped logic against grr.Default unless every caller guarantees a scope.
Example ¶
RegisterScopedIn builds the model once per active scope. Two calls within the same scope share an instance; a new scope gets a new build.
package main
import (
"context"
"fmt"
"github.com/arpaad/grr"
gold "github.com/arpaad/gold"
)
type greetRequest string
type greetResponse struct{ Text string }
type greeter interface {
Greet() string
}
type greetLogic struct{ greeter }
func newGreet(m greeter) *greetLogic { return &greetLogic{greeter: m} }
func (l *greetLogic) Do(_ context.Context, req greetRequest) (*greetResponse, error) {
return &greetResponse{Text: l.Greet() + string(req)}, nil
}
type staticGreeter struct{ msg string }
func (s *staticGreeter) Greet() string { return s.msg }
func main() {
r := grr.New()
Greet := gold.NewLogic("Greet", newGreet)
builds := 0
Greet.RegisterScopedIn(r, func(_ context.Context) greeter {
builds++
return &staticGreeter{msg: "Hi"}
})
base := grr.WithRegistry(context.Background(), r)
ctx1, end1 := r.BeginScope(base)
_, _ = Greet.Do(ctx1, greetRequest(""))
_, _ = Greet.Do(ctx1, greetRequest(""))
end1()
fmt.Println(builds) // 1 — reused within scope
ctx2, end2 := r.BeginScope(base)
defer end2()
_, _ = Greet.Do(ctx2, greetRequest(""))
fmt.Println(builds) // 2 — new scope, new build
}
Output: 1 2
func (*Logic[request, response, model, impl]) RegisterSingleton ¶
func (l *Logic[request, response, model, impl]) RegisterSingleton(newModel func() model)
RegisterSingleton is sugar for RegisterSingletonIn(grr.Default, newModel).
func (*Logic[request, response, model, impl]) RegisterSingletonIn ¶
func (l *Logic[request, response, model, impl]) RegisterSingletonIn(r *grr.Registry, newModel func() model)
RegisterSingletonIn builds the model once (guarded by sync.Once) and reuses the resulting impl for every Do call resolved against r.
Example ¶
RegisterSingletonIn builds the model exactly once regardless of how many times Do is called.
package main
import (
"context"
"fmt"
"github.com/arpaad/grr"
gold "github.com/arpaad/gold"
)
type greetRequest string
type greetResponse struct{ Text string }
type greeter interface {
Greet() string
}
type greetLogic struct{ greeter }
func newGreet(m greeter) *greetLogic { return &greetLogic{greeter: m} }
func (l *greetLogic) Do(_ context.Context, req greetRequest) (*greetResponse, error) {
return &greetResponse{Text: l.Greet() + string(req)}, nil
}
type staticGreeter struct{ msg string }
func (s *staticGreeter) Greet() string { return s.msg }
func main() {
r := grr.New()
Greet := gold.NewLogic("Greet", newGreet)
builds := 0
Greet.RegisterSingletonIn(r, func() greeter {
builds++
return &staticGreeter{msg: "Hi"}
})
ctx := grr.WithRegistry(context.Background(), r)
_, _ = Greet.Do(ctx, greetRequest(""))
_, _ = Greet.Do(ctx, greetRequest(""))
fmt.Println(builds)
}
Output: 1
func (*Logic[request, response, model, impl]) RegisterTransient ¶
func (l *Logic[request, response, model, impl]) RegisterTransient(newModel func(ctx context.Context) model)
RegisterTransient is sugar for RegisterTransientIn(grr.Default, newModel).
func (*Logic[request, response, model, impl]) RegisterTransientIn ¶
func (l *Logic[request, response, model, impl]) RegisterTransientIn(r *grr.Registry, newModel func(ctx context.Context) model)
RegisterTransientIn builds a fresh model (and impl) on every Do call.
Example ¶
RegisterTransientIn builds a fresh model on every Do call.
package main
import (
"context"
"fmt"
"github.com/arpaad/grr"
gold "github.com/arpaad/gold"
)
type greetRequest string
type greetResponse struct{ Text string }
type greeter interface {
Greet() string
}
type greetLogic struct{ greeter }
func newGreet(m greeter) *greetLogic { return &greetLogic{greeter: m} }
func (l *greetLogic) Do(_ context.Context, req greetRequest) (*greetResponse, error) {
return &greetResponse{Text: l.Greet() + string(req)}, nil
}
type staticGreeter struct{ msg string }
func (s *staticGreeter) Greet() string { return s.msg }
func main() {
r := grr.New()
Greet := gold.NewLogic("Greet", newGreet)
builds := 0
Greet.RegisterTransientIn(r, func(_ context.Context) greeter {
builds++
return &staticGreeter{msg: "Hi"}
})
ctx := grr.WithRegistry(context.Background(), r)
_, _ = Greet.Do(ctx, greetRequest(""))
_, _ = Greet.Do(ctx, greetRequest(""))
fmt.Println(builds)
}
Output: 2
func (*Logic[request, response, model, impl]) TryDo ¶
func (l *Logic[request, response, model, impl]) TryDo(ctx context.Context, req request) (resp response, ok bool, err error)
TryDo is Do but reports a missing implementation with ok == false instead of panicking — for genuinely optional, feature-flagged logic that may or may not be wired. ok is false ONLY when no implementation is registered. A registered-but-wrong-type implementation still panics (a wiring bug), and a business-logic error still flows through err.
type Logical ¶
type Logical[request, response any] interface { Do(ctx context.Context, req request) (response, error) }
Logical is what a concrete logic implementation must satisfy.
type MissingError ¶
type MissingError struct {
Names []string
}
MissingError reports elements (logics or ports) that were declared with NewLogic/NewPort but have no implementation registered in the validated registry. Names is sorted.
func (*MissingError) Error ¶
func (e *MissingError) Error() string
type Port ¶
type Port[T any] struct { // contains filtered or unexported fields }
Port is a typed, named, lifecycle-managed binding to a value of type T held in a grr.Registry. T is unconstrained — an interface, a struct, a pointer, anything — which makes Port the general-purpose typed DI primitive: it gives a name and a compile-time-checked type to a dependency (a config, a *sql.DB, an HTTP client, a request-scoped data object with several Get* methods) and lets its implementation be wired separately from the call site.
Logic is a specialization of Port: a Logic is a Port whose T has a Do method, plus model→impl construction sugar. Reach for Port for plain collaborator/resource dependencies, and for Logic when the dependency is a unit of domain behaviour invoked as Do(ctx, request).
The name keys the binding in grr's storage and doubles as its stable, human-readable address — used by grr's OnResolve hook for metric labels, by Validate at startup, and by grr.Keys introspection. Prefer namespaced names ("billing.CurrentUser") to avoid collisions across packages.
Resolve(ctx) is the single accessor, and it necessarily takes a ctx: a scoped value's instance is selected from the scope carried in ctx, and a stored value's methods can't carry one themselves. Resolve at a boundary (e.g. the top of a handler) and pass the value down by parameter.
func NewPort ¶
NewPort declares a port. It does not register anything — wiring happens separately via Register/RegisterIn and the lifetime sugar below. The name is registered with Validate so a declared-but-unwired port is caught at startup, the same way logics are.
Example ¶
NewPort gives a name and a compile-time-checked type to a dependency, wired separately from the definition. Here a process-wide singleton config.
package main
import (
"context"
"fmt"
"github.com/arpaad/grr"
gold "github.com/arpaad/gold"
)
type appConfig struct{ Greeting string }
func main() {
r := grr.New()
Config := gold.NewPort[*appConfig]("config")
Config.RegisterSingletonIn(r, func() *appConfig {
return &appConfig{Greeting: "Hello"}
})
ctx := grr.WithRegistry(context.Background(), r)
cfg := Config.Resolve(ctx)
fmt.Println(cfg.Greeting)
}
Output: Hello
func (*Port[T]) IsRegistered ¶
IsRegistered reports whether a value is wired for this port in ctx's registry (or its parent chain).
func (*Port[T]) RegisterIn ¶
RegisterIn is the base registration primitive — full freedom over how the value is produced per resolve (external cache, A/B test, tenant routing). The Register* sugar below all reduce to this.
func (*Port[T]) RegisterScoped ¶
RegisterScoped is sugar for RegisterScopedIn(grr.Default, newVal).
func (*Port[T]) RegisterScopedIn ¶
RegisterScopedIn builds the value once per active scope (see grr.Registry.BeginScope) and reuses it for the rest of that scope. Resolving without an active scope panics — never register a scoped port against grr.Default unless every caller guarantees a scope.
Example ¶
A scoped port builds one instance per active scope — ideal for request-scoped data with several accessors. Resolve once at the boundary, then use the value directly.
package main
import (
"context"
"fmt"
"github.com/arpaad/grr"
gold "github.com/arpaad/gold"
)
// sessionUser is a multi-method, data-centric dependency — not Do-shaped, so
// it's a Port rather than a Logic.
type sessionUser interface {
Name() string
Tier() string
}
type sessionUserImpl struct {
name string
tier string
}
func (u *sessionUserImpl) Name() string { return u.name }
func (u *sessionUserImpl) Tier() string { return u.tier }
func main() {
r := grr.New()
CurrentUser := gold.NewPort[sessionUser]("currentUser")
CurrentUser.RegisterScopedIn(r, func(_ context.Context) sessionUser {
return &sessionUserImpl{name: "ada", tier: "pro"}
})
base := grr.WithRegistry(context.Background(), r)
ctx, end := r.BeginScope(base)
defer end()
u := CurrentUser.Resolve(ctx)
fmt.Println(u.Name(), u.Tier())
}
Output: ada pro
func (*Port[T]) RegisterSingleton ¶
func (p *Port[T]) RegisterSingleton(newVal func() T)
RegisterSingleton is sugar for RegisterSingletonIn(grr.Default, newVal).
func (*Port[T]) RegisterSingletonIn ¶
RegisterSingletonIn builds the value once (guarded by sync.Once) and reuses it for every Resolve against r.
func (*Port[T]) RegisterTransient ¶
RegisterTransient is sugar for RegisterTransientIn(grr.Default, newVal).
func (*Port[T]) RegisterTransientIn ¶
RegisterTransientIn builds a fresh value on every Resolve. It is the transient-lifetime alias of RegisterIn — same mechanics, intent-revealing name.
func (*Port[T]) Resolve ¶
Resolve looks up the value wired for this port in ctx's registry (falling back to grr.Default) and returns it as T. Panics if nothing is registered for this port — that's a wiring bug, not a runtime condition to branch on (use TryResolve for genuinely optional ports, or IsRegistered to check first).
func (*Port[T]) TryResolve ¶
TryResolve is Resolve but reports a missing wiring with ok == false instead of panicking — for genuinely optional, feature-flagged ports. ok is false ONLY when nothing is registered; a registered-but-wrong-type factory still panics (a wiring bug).
Example ¶
TryResolve reports a missing wiring with ok == false instead of panicking — for genuinely optional, feature-flagged ports.
package main
import (
"context"
"fmt"
"github.com/arpaad/grr"
gold "github.com/arpaad/gold"
)
type appConfig struct{ Greeting string }
func main() {
r := grr.New()
Optional := gold.NewPort[*appConfig]("optionalConfig")
ctx := grr.WithRegistry(context.Background(), r)
if _, ok := Optional.TryResolve(ctx); !ok {
fmt.Println("not wired")
}
}
Output: not wired