dscope

package module
v0.0.0-...-33578e8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 20, 2026 License: MIT Imports: 12 Imported by: 34

README

dscope - A Dependency Injection Library for Go

dscope is a powerful and flexible dependency injection library for Go, designed to promote clean architecture, enhance testability, and manage dependencies with ease. It emphasizes immutability, lazy initialization, and type safety.

Why use dscope?

Managing dependencies in larger Go applications can become complex. dscope offers several advantages:

  • Type-Safe Dependencies: Leverages Go's type system to ensure that dependencies are resolved correctly at compile time or with clear runtime panics if a type is missing. Generic functions like Get[T](scope) provide compile-time type checking for retrievals.
  • Define and Depend on Interfaces (or Concrete Types): While you can register and request concrete types directly, dscope fully supports defining providers that return interfaces and requesting dependencies via those interfaces, promoting loose coupling.
  • Cleaner Function Signatures: The scope.Call(yourFunction) feature automatically resolves the arguments for yourFunction from the scope. This means yourFunction only needs to declare its essential operational arguments, not a long list of dependencies it needs to acquire manually.
  • Enhanced Testability:
    • Immutability: Scopes are immutable. Operations like Fork create new scopes, leaving the original untouched. This predictability is great for testing.
    • Easy Overriding: You can easily Fork a scope and provide alternative (mock or stub) implementations for specific types, making unit and integration testing more straightforward.
  • Immutable and Predictable Scopes: Each scope is an immutable container. Modifying a scope (e.g., adding new definitions or overriding existing ones) results in a new scope instance. This makes the state of dependencies predictable and easier to reason about.
  • Lazy Initialization: Values within a scope are initialized lazily. A provider function is only called when the value it provides (or a dependant value) is actually requested for the first time. This can improve application startup time and resource usage.
  • Fine-Grained Recomputation: Fork recomputes only the overridden type and its transitive dependents. Prefer single-value types over composites with multiple mutable fields: overriding one value then recomputes only the providers that depend on it, not every consumer of the composite.

Core Concepts

Scope

A Scope is an immutable container holding definitions for various types. It's the central piece from which you resolve dependencies. The Universe is the initial empty scope.

Definitions

Definitions are functions or pointers that tell a scope how to create or provide an instance of a type.

  • Provider Functions: Functions that return one or more values. Their arguments are themselves resolved as dependencies from the scope.
    func NewMyService(db Database) MyService { /* ... */ }
    func NewConfigAndLogger() (Config, Logger) { /* ... */ }
    
  • Pointer Values: Pointers to existing instances can also be used as definitions. The pointed-at value will be provided.
    cfg := &MyConfig{Value: "example"}
    scope := dscope.New(cfg) // MyConfig is now available
    
Fork

Fork is the primary mechanism for creating new scopes. When you Fork an existing scope, you create a new scope that contains every definition of the original, with the new definitions layered on top. The two scopes are independent branches: you can add new definitions or override existing ones in the new scope without affecting the original.

baseScope := dscope.New(func() int { return 42 })
branch1 := baseScope.Fork(func() string { return "hello" }) // same int as base, adds string
branch2 := baseScope.Fork(func() int { return 100 })   // overrides int
Reset

Reset creates a new scope in which every provider result is recomputed lazily on the next access. The original scope is unaffected.

calls := 0
scope := dscope.New(func() int {
	calls++
	return calls
})
fmt.Println(dscope.Get[int](scope))      // Output: 1
fmt.Println(dscope.Get[int](scope))      // Output: 1 (result is cached)

resetScope := scope.Reset()
fmt.Println(dscope.Get[int](resetScope)) // Output: 2 (recomputed)

dscope.Reset (type Reset func() Scope) is also provided as a built-in dependency bound to the current scope's Reset method, so it can be injected into providers, similar to dscope.Fork:

scope.Call(func(reset dscope.Reset) {
	// reset() returns a reset scope of the current scope
})
```## Basic Usage Examples

### 1. Creating a New Scope

You can create a new scope from scratch using `dscope.New()` (which is equivalent to `dscope.Universe.Fork()`).

```go
package main

import (
	"fmt"
	"github.com/reusee/dscope"
)

// Define some types
type Greeter string
type Message string

// Define provider functions
func provideGreeter() Greeter {
	return "Hello"
}

func provideMessage(g Greeter) Message {
	return Message(fmt.Sprintf("%s, dscope!", g))
}

func main() {
	scope := dscope.New(
		provideGreeter,
		provideMessage,
	)
	// Scope is now configured
}
2. Getting Values by Type

You can retrieve values from the scope using scope.GetType(reflect.Type), the generic dscope.Get[T](scope), or scope.Assign(pointers...).

  • dscope.Get[T](scope) (Recommended for type safety):

    msg := dscope.Get[Message](scope)
    fmt.Println(msg) // Output: Hello, dscope!
    
  • scope.TryGet[T]() (Non-panicking lookup): returns the value and whether the type is defined in the scope, instead of panicking on a missing type.

    if msg, ok := scope.TryGet[Message](); ok {
        fmt.Println(msg) // Output: Hello, dscope!
    }
    
  • scope.Assign(pointers...):

    var m Message
    var g Greeter
    scope.Assign(&m, &g)
    fmt.Println(g, m) // Output: Hello Hello, dscope!
    
  • scope.GetType(reflect.Type) / scope.TryGetType(reflect.Type) (reflection-based lookup; GetType panics on a missing type):

    import "reflect"
    // ...
    msgVal, ok := scope.TryGetType(reflect.TypeOf(Message("")))
    if ok {
        fmt.Println(msgVal.Interface().(Message))
    }
    
3. Calling Functions in a Scope

scope.Call(fn) executes fn, automatically resolving its arguments from the scope. Return values are wrapped in a CallResult.

type Salutation string

func provideSalutation() Salutation {
	return "Greetings"
}

scope = scope.Fork(provideSalutation) // Add Salutation to the scope

result := scope.Call(func(s Salutation, m Message) string {
	return fmt.Sprintf("%s! %s", s, m)
})

var finalMsg string
result.Assign(&finalMsg) // Assigns the string return value
// or result.Extract(&finalMsg) if order matters and you know the return position

fmt.Println(finalMsg) // Output: Greetings! Hello, dscope!```

### 4. Forking a Scope

Forking creates a new scope that contains the original's definitions, allowing you to add or override definitions.

```go
baseScope := dscope.New(func() int { return 10 })

// Fork 1: Add a new type
branch1 := baseScope.Fork(func(i int) string {
	return fmt.Sprintf("Number: %d", i)
})
fmt.Println(dscope.Get[string](branch1)) // Output: Number: 10

// Fork 2: Override an existing type
branch2 := baseScope.Fork(func() int { return 20 })
fmt.Println(dscope.Get[int](branch2)) // Output: 20

// Original scope is unaffected
fmt.Println(dscope.Get[int](baseScope)) // Output: 10
5. Modules

Modules help organize definitions. You can embed dscope.Module in your structs and then use dscope.Methods(moduleInstances...) to add all exported methods of those instances (and their embedded modules) as providers to the scope.

type DatabaseModule struct {
	dscope.Module
}

func (dbm *DatabaseModule) ProvideDBConnection() string { // Becomes a provider
	return "db_connection_string"
}

type ServiceModule struct {
	dscope.Module
	DBDep DatabaseModule // Embedded module's methods will also be added
}

func (sm *ServiceModule) ProvideMyService(dbConn string) string { // Becomes a provider
	return "service_using_" + dbConn
}

func main() {
	// Using concrete instance
	scope := dscope.New(
		dscope.Methods(new(ServiceModule))...,
	)
	service := dscope.Get[string](scope) // Will try to get "service_using_db_connection_string"
	fmt.Println(service)
}

Output:

service_using_db_connection_string

You can also pass instances of structs that embed dscope.Module directly to New or Fork:

type ModA struct {
    dscope.Module
}
func (m ModA) GetA() string { return "A from ModA" }

type ModB struct {
    dscope.Module
    MyModA ModA // ModA's methods will be included
}
func (m ModB) GetB() string { return "B from ModB" }


func main() {
    scope := dscope.New(
        new(ModB), // Automatically uses Methods() for types embedding dscope.Module
    )
    fmt.Println(dscope.Get[string](scope, dscope.WithTypeQualifier("GetA"))) // Assuming a way to qualify if GetA and GetB return string
    // For distinct return types, direct Get[T] works:
    // e.g. if GetA returns type AVal and GetB returns type BVal
    // aVal := dscope.Get[AVal](scope)
    // bVal := dscope.Get[BVal](scope)
}

Note: The example with dscope.WithTypeQualifier is illustrative if multiple providers return the same type. If return types are unique, dscope.Get[ReturnType] is sufficient. dscope primarily resolves by type.

6. Struct Field Injection

dscope can inject dependencies into the fields of a struct.

  • Using dscope:"." or dscope:"inject" tag:

    type MyStruct struct {
        Dep1 Greeter `dscope:"."` // or dscope:"inject"
        Dep2 Message `dscope:"."`
    }
    
    scope := dscope.New(provideGreeter, provideMessage)
    var myInstance MyStruct
    // scope.Call(func(inject dscope.InjectStruct) { inject(&myInstance) })
    // OR directly:
    scope.InjectStruct(&myInstance)
    
    
    fmt.Printf("Injected: Greeter='%s', Message='%s'\n", myInstance.Dep1, myInstance.Dep2)
    // Output: Injected: Greeter='Hello', Message='Hello, dscope!'
    
  • Using dscope.Inject[T] for lazy field injection: Fields of type dscope.Inject[T] are populated with a function that, when called, resolves T from the scope. This is useful for optional dependencies or dependencies needed much later.

    type AnotherStruct struct {
        LazyGreeter dscope.Inject[Greeter]
        RegularMsg  Message `dscope:"."`
    }
    
    scope := dscope.New(provideGreeter, provideMessage)
    var anotherInstance AnotherStruct
    scope.InjectStruct(&anotherInstance)
    
    fmt.Printf("Regular Message: %s\n", anotherInstance.RegularMsg)
    // LazyGreeter is not resolved yet.
    // To get the greeter:
    actualGreeter := anotherInstance.LazyGreeter()
    fmt.Printf("Lazy Greeter: %s\n", actualGreeter)
    // Output:
    // Regular Message: Hello, dscope!
    // Lazy Greeter: Hello
    

This covers the core features and usage patterns of dscope. Its design promotes modularity and testability in Go applications.

Documentation

Index

Constants

View Source
const TheoryOfCallResult = `` /* 553-byte string literal not displayed */

TheoryOfCallResult documents how call return values reach their targets.

View Source
const TheoryOfConstructors = `` /* 1150-byte string literal not displayed */
View Source
const TheoryOfLazyInitialization = `` /* 889-byte string literal not displayed */

TheoryOfLazyInitialization documents the design rationale for dscope's lazy initialization mechanism. Provider functions are evaluated on first access; results are cached and shared across all consumers within the same scope. A panicking provider must NOT permanently cache the failure — subsequent accesses must re-invoke the provider to reproduce the original error, ensuring that transient provider failures are always diagnosable and never leave the system in an unrecoverable or misleading state.

View Source
const TheoryOfModuleMethodDiscovery = `` /* 712-byte string literal not displayed */

TheoryOfModuleMethodDiscovery documents how Methods expands a module object into provider functions and which inputs are rejected.

View Source
const TheoryOfModules = `` /* 890-byte string literal not displayed */

TheoryOfModules documents the module pattern: grouping providers as methods on a struct that embeds dscope.Module.

View Source
const TheoryOfScopeAssignment = `` /* 1095-byte string literal not displayed */

TheoryOfScopeAssignment documents the retrieval semantics shared by the assignment entry points.

View Source
const TheoryOfScopeCore = `` /* 1505-byte string literal not displayed */

TheoryOfScopeCore documents the fundamental model of dscope: an immutable, type-keyed container of lazily evaluated definitions.

View Source
const TheoryOfScopeDefinitions = `` /* 1137-byte string literal not displayed */
View Source
const TheoryOfScopeFork = `` /* 1304-byte string literal not displayed */

TheoryOfScopeFork documents the semantics and typical uses of Fork.

View Source
const TheoryOfScopeForkFlatten = `` /* 732-byte string literal not displayed */
View Source
const TheoryOfScopeForkValue = `` /* 655-byte string literal not displayed */
View Source
const TheoryOfScopeInjectStruct = `` /* 992-byte string literal not displayed */

TheoryOfScopeInjectStruct documents struct field injection: how fields are selected and how the built-in binding behaves.

View Source
const TheoryOfScopeInvocation = `` /* 687-byte string literal not displayed */

TheoryOfScopeInvocation documents the semantics of scope.Call and the validation applied to call targets.

View Source
const TheoryOfScopeReset = `` /* 806-byte string literal not displayed */

TheoryOfScopeReset documents the semantics and typical use of Reset.

View Source
const TheoryOfScopeResetValue = `` /* 674-byte string literal not displayed */
View Source
const TheoryOfScopeVisualization = `` /* 279-byte string literal not displayed */
View Source
const TheoryOfTypeGranularity = `` /* 570-byte string literal not displayed */

Variables

View Source
var ErrBadArgument = errors.New("bad argument")
View Source
var ErrBadDefinition = errors.New("bad definition")
View Source
var ErrDependencyLoop = errors.New("dependency loop")
View Source
var ErrDependencyNotFound = errors.New("dependency not found")
View Source
var Universe = Scope{}

Universe is the empty root scope.

Functions

func Methods

func Methods(objects ...any) (ret []any)

func Provide

func Provide[T any](v T) *T

Provide is a helper that returns a pointer to a value, suitable for use as a definition in a Scope. The value is copied when the scope is created.

Types

type CallResult

type CallResult struct {
	Values []reflect.Value
}

func (CallResult) Assign

func (c CallResult) Assign(targets ...any)

func (CallResult) Extract

func (c CallResult) Extract(targets ...any)

Extract extracts results by positions

type Fork

type Fork func(defs ...any) Scope

Fork is the type of a scope's Fork method, bound to the scope that provides it. It is always provided as a built-in dependency.

type Inject

type Inject[T any] func() T

type InjectStruct

type InjectStruct func(target any)

type Module

type Module struct{}

type Reset

type Reset func() Scope

Reset is the type of a scope's Reset method, bound to the scope that provides it. It is always provided as a built-in dependency.

type Scope

type Scope struct {
	// contains filtered or unexported fields
}

Scope represents an immutable dependency injection container. Operations like Fork create new Scope values.

func New

func New(
	defs ...any,
) Scope

New creates a new root Scope with the given definitions. Equivalent to Universe.Fork(defs...).

func (Scope) AllTypes

func (s Scope) AllTypes() iter.Seq[reflect.Type]

func (Scope) Assign

func (scope Scope) Assign[T any](ptr *T)

Assign retrieves the value of type T from the scope and writes it to the provided pointer. It panics if ptr is nil or if the type is not found. It's safe to call Assign concurrently.

func (Scope) Call

func (scope Scope) Call(fn any) CallResult

Call executes the given function `fn`, resolving its arguments from the scope. It returns a CallResult containing the return values of the function. Panics if argument resolution fails or if `fn` is not a function.

func (Scope) CallValue

func (scope Scope) CallValue(fnValue reflect.Value) (res CallResult)

CallValue executes the given function value after resolving its arguments from the scope. It returns a CallResult containing the function's return values. It panics with ErrBadArgument if fnValue is invalid, nil, or not a function.

func (Scope) Fork

func (scope Scope) Fork(
	defs ...any,
) Scope

Fork creates a new scope by layering the given definitions (`defs`) on top of the current scope's definitions. The result is a new branch of the same definition lineage: scopes have no child-parent relationship, and the original scope is never mutated. Fork handles overriding existing definitions and ensures values are lazily initialized.

Definitions can be provider functions or pointers to values. When a pointer is provided, the value it points to is copied; subsequent changes to the original variable will not affect the value in the scope. To provide a shared singleton, use a provider function that returns a pointer.

func (Scope) Get

func (scope Scope) Get[T any]() T

Get is a type-safe generic method to retrieve a single value of type T. It panics if the type is not found or if an error occurs during resolution.

func (Scope) GetType

func (scope Scope) GetType(typ reflect.Type) reflect.Value

GetType retrieves the value of the given type from the scope and returns it as a reflect.Value. It panics with a structured dependency-not-found error if the type is not defined in the scope, mirroring the generic Get[T].

func (Scope) InjectStruct

func (scope Scope) InjectStruct(target any)

func (Scope) Reset

func (scope Scope) Reset() Scope

Reset returns a new Scope in which every value will be recomputed the next time it is requested. The original scope is unaffected.

Reset is O(1): it wraps the value stack in a lazy reset layer rather than eagerly iterating all definitions. Fresh initializers are created on demand only for types that are actually accessed, so untouched types incur zero overhead. Once a provider is re-evaluated in the reset scope the result is cached, preserving the at-most-once evaluation guarantee.

func (Scope) ToDOT

func (scope Scope) ToDOT(w io.Writer) error

func (Scope) TryGet

func (scope Scope) TryGet[T any]() (o T, ok bool)

TryGet is a type-safe generic method to retrieve a single value of type T. Unlike Get, it does not panic when the type is missing from the scope: it returns (zero, false) instead. Panics raised by provider evaluation still propagate.

func (Scope) TryGetType

func (scope Scope) TryGetType(typ reflect.Type) (reflect.Value, bool)

TryGetType retrieves the value of the given type from the scope as a reflect.Value. Unlike GetType, it does not panic when the type is missing from the scope: it returns the zero reflect.Value and false instead. Panics raised by provider evaluation still propagate.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL