dscope

package module
v0.0.0-...-93feff4 Latest Latest
Warning

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

Go to latest
Published: Apr 22, 2025 License: MIT Imports: 15 Imported by: 34

README

dscope - An Immutable Dependency Injection Library for Go

dscope is a dependency injection library for Go focused on immutability, lazy initialization, and type safety. It provides a flexible way to manage dependencies within your application.

Why Use dscope?

  • Type-Safe Dependencies: Leverages Go's type system. Generic functions like Get[T] and Assign[T] provide compile-time safety, while other operations perform runtime type checks.
  • Promotes Loose Coupling: Dependencies are resolved based on their type (including interface types), allowing you to depend on abstractions rather than concrete implementations.
  • Enhanced Testability: Easily swap implementations for testing by forking scopes and providing mock definitions.
  • Immutable and Predictable: Scopes are immutable. Operations like Fork create new Scope values, preserving the state of the original. This makes reasoning about dependency states simpler and safer, especially in concurrent environments.
  • Lazy Initialization: Values are computed only when they are first requested, thanks to sync.Once. This improves performance by avoiding unnecessary initialization work.
  • Concurrency Safe: Designed for concurrent use. Reading values (Get, Assign, Call) and creating new scopes (Fork) from existing scopes can be done safely from multiple goroutines.
  • Performance Conscious: While using reflection, dscope employs internal caching for fork operations, function argument lookups, and return type analysis to mitigate performance overhead in common scenarios.

Core Concepts

  1. Scope:

    • The central concept in dscope. A Scope is an immutable container that holds dependency definitions and resolved values.
    • The root scope is dscope.Universe. New scopes are typically created using dscope.New(...) (which is equivalent to dscope.Universe.Fork(...)).
  2. Definitions:

    • These tell the scope how to create or provide values. Definitions are passed as arguments to New or Fork.
    • Functions: The most common way. A function like func(depA A, depB B) C { ... } defines how to create a value of type C, declaring its dependencies (A and B) as arguments. Functions can also return multiple values (func() (A, B)), defining providers for both A and B.
    • Pointers: Providing a pointer like *T makes the pointed-to value available in the scope with type T. Example: i := 42; scope := dscope.New(&i) makes an int available.
  3. Fork:

    • The primary operation for creating new scopes from existing ones. parentScope.Fork(newDefs...) creates a new child scope.
    • Definitions in the child scope layer on top of the parent's definitions.
    • If a child defines a provider for a type already defined in the parent, the child's definition overrides the parent's for that child scope and its descendants. The parent scope remains unchanged.
    • Forking is efficient due to internal caching (_Forker).

Installation

go get github.com/reusee/dscope

Basic Usage Examples

Create a New Scope
package main

import "github.com/reusee/dscope"

type Config struct {
    ConnectionString string
}

func provideConfig() Config {
    return Config{ConnectionString: "localhost:5432"}
}

func main() {
    // Create a scope with a Config provider
    scope := dscope.New(
        provideConfig, // Function provider
    )
    // scope now knows how to provide Config
}
Get Values by Type

Values are retrieved lazily when requested.

package main

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

type ServiceA struct {
    Dep ServiceB
}

type ServiceB struct{}

func provideServiceA(b ServiceB) ServiceA {
    fmt.Println("Providing ServiceA")
    return ServiceA{Dep: b}
}

func provideServiceB() ServiceB {
    fmt.Println("Providing ServiceB")
    return ServiceB{}
}

func main() {
    scope := dscope.New(
        provideServiceA,
        provideServiceB,
    )

    // Using generic Get (panics if not found)
    fmt.Println("Getting ServiceA...")
    serviceA := dscope.Get[ServiceA](scope)
    fmt.Printf("Got ServiceA: %+v\n", serviceA)

    // Using Assign (panics if not found or target is not pointer)
    fmt.Println("\nAssigning ServiceB...")
    var serviceB ServiceB
    scope.Assign(&serviceB) // Resolves ServiceB
    fmt.Printf("Assigned ServiceB: %+v\n", serviceB)

    // Requesting ServiceA again - provider is NOT called again
    fmt.Println("\nGetting ServiceA again...")
    serviceA2 := dscope.Get[ServiceA](scope)
    fmt.Printf("Got ServiceA again: %+v\n", serviceA2)
}

/* Output:
Getting ServiceA...
Providing ServiceA
Providing ServiceB
Got ServiceA: {Dep:{}}

Assigning ServiceB...
Assigned ServiceB: {}

Getting ServiceA again...
Got ServiceA again: {Dep:{}}
*/
Calling Functions in a Scope

scope.Call executes a function, automatically resolving its arguments from the scope.

package main

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

type UserID string
type Session struct{ User UserID }

func main() {
    scope := dscope.New(
        func() UserID { return "user-123" },
        func(id UserID) Session { return Session{User: id} },
    )

    // Call a function requiring Session and UserID
    scope.Call(func(s Session, id UserID) {
        fmt.Printf("Called function with Session: %+v, UserID: %s\n", s, id)
        // Output: Called function with Session: {User:user-123}, UserID: user-123
    })

    // Call a function that returns values
    var result string
    scope.Call(func(s Session) string {
        return fmt.Sprintf("User from session: %s", s.User)
    }).Assign(&result) // Assign the string return value to 'result'

    fmt.Println(result) // Output: User from session: user-123
}
Fork a Scope

Forking creates a new scope, layering definitions.

package main

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

type Database string

func main() {
    baseScope := dscope.New(
        func() Database { return "ProductionDB" },
        func() int { return 100 }, // Some other value
    )

    // Fork to create a testing scope with a different database
    testScope := baseScope.Fork(
        func() Database { return "TestDB" },
    )

    // Get Database from both scopes
    prodDB := dscope.Get[Database](baseScope)
    testDB := dscope.Get[Database](testScope)

    fmt.Printf("Base Scope DB: %s\n", prodDB) // Output: Base Scope DB: ProductionDB
    fmt.Printf("Test Scope DB: %s\n", testDB) // Output: Test Scope DB: TestDB

    // Other values are inherited
    baseInt := dscope.Get[int](baseScope)
    testInt := dscope.Get[int](testScope)
    fmt.Printf("Base Scope Int: %d\n", baseInt) // Output: Base Scope Int: 100
    fmt.Printf("Test Scope Int: %d\n", testInt) // Output: Test Scope Int: 100
}
Modules

Group related definitions using structs embedding dscope.Module.

package main

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

// --- Module Definition ---
type AuthModule struct {
	dscope.Module // Embed Module
	Secrets       SecretProvider `dscope:"."` // Include methods from SecretProvider field
}

func (m AuthModule) AuthService(sp SecretProvider) string {
	return "AuthService using " + sp.GetSecret()
}

type SecretProvider struct{}

func (sp SecretProvider) GetSecret() string {
	return "secret-key"
}
// --- End Module Definition ---


func main() {
    // Register the module instance directly
    scope := dscope.New(
        new(AuthModule), // Pass module instance
        // Alternatively: dscope.Methods(new(AuthModule))...
    )

    // Dependencies provided by the module are available
    authSvc := dscope.Get[string](scope) // Assuming AuthService is the only string provider
    fmt.Println(authSvc) // Output: AuthService using secret-key

    secret := dscope.Get[SecretProvider](scope).GetSecret()
    fmt.Println(secret) // Output: secret-key
}
Struct Field Injection

Automatically inject dependencies into struct fields tagged with dscope.

package main

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

type Logger struct{ Prefix string }
type Handler struct {
	Log Logger `dscope:"."` // Inject Logger here
	DB  string `dscope:"inject"` // Alternative tag
}

func main() {
    scope := dscope.New(
        func() Logger { return Logger{Prefix: "[INFO]"} },
        func() string { return "PostgresConnection" }, // Provides the DB string
    )

    // Option 1: Get InjectStruct function from scope
    injector := dscope.Get[dscope.InjectStruct](scope)
    var handler1 Handler
    err := injector(&handler1)
    if err != nil {
        panic(err)
    }
    fmt.Printf("Handler 1: %+v\n", handler1)
    // Output: Handler 1: {Log:{Prefix:[INFO]} DB:PostgresConnection}

    // Option 2: Use scope.InjectStruct directly (if available - depends on how scope was created)
    // Note: InjectStruct is implicitly available.
    var handler2 Handler
    err = scope.InjectStruct(&handler2)
     if err != nil {
        panic(err)
    }
    fmt.Printf("Handler 2: %+v\n", handler2)
    // Output: Handler 2: {Log:{Prefix:[INFO]} DB:PostgresConnection}
}

Concurrency

dscope is designed to be safe for concurrent use:

  • Reading from a Scope (Get, Assign, Call) is safe from multiple goroutines.
  • Forking a Scope (Fork) is safe from multiple goroutines.
  • Initialization of values is handled safely via sync.Once.

Debugging

dscope provides tools to help understand the dependency graph:

  • scope.ToDOT(io.Writer): Generates a graph description in the DOT language (usable with Graphviz) showing the effective providers and their dependencies.
  • scope.GetDebugInfo(): Returns structured information about the types defined in the scope and their provider function types.

Documentation

Index

Constants

This section is empty.

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 Assign

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

Assign is a type-safe generic wrapper for Scope.Assign for a single pointer.

func Get

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

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

func Methods

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

Methods extracts all exported methods from the provided objects and any fields tagged with `dscope:"."` or `dscope:"methods"`. It recursively traverses struct fields marked for extension to collect their methods as well. This prevents infinite loops by keeping track of visited types.

func Provide

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

Types

type CallResult

type CallResult struct {
	Values []reflect.Value
	// contains filtered or unexported fields
}

func (CallResult) Assign

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

func (CallResult) Extract

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

type DebugInfo

type DebugInfo struct {
	Values map[reflect.Type]ValueDebugInfo
}

type Fork

type Fork func(defs ...any) Scope

type InjectStruct

type InjectStruct func(target any) error

type Module

type Module struct{}

type Path

type Path struct {
	Prev   *Path
	TypeID _TypeID
}

func (Path) Error

func (p Path) Error() string

func (*Path) String

func (p *Path) String() string

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(objects ...any)

Assign retrieves values from the scope matching the types of the provided pointers and assigns the values to the pointers. It panics if any argument is not a pointer or if a required 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 `fnValue` after resolving its arguments from the scope. It returns a CallResult containing the function's return values.

func (Scope) Fork

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

Fork creates a new child scope by layering the given definitions (`defs`) on top of the current scope. It handles overriding existing definitions and ensures values are lazily initialized.

func (Scope) Get

func (scope Scope) Get(t reflect.Type) (
	ret reflect.Value,
	ok bool,
)

Get retrieves a single value of the specified type `t` from the scope. It panics if the type is not found or if an error occurs during resolution. Use Assign or the generic Get[T] for safer retrieval.

func (Scope) GetDebugInfo

func (s Scope) GetDebugInfo() (info DebugInfo)

func (Scope) InjectStruct

func (scope Scope) InjectStruct(target any) error

func (Scope) ToDOT

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

ToDOT generates a DOT language representation of the scope's dependency graph. This output can be used with tools like Graphviz to visualize the scope's structure. It shows the effective definition for each type and its direct dependencies.

type ValueDebugInfo

type ValueDebugInfo struct {
	DefTypes []reflect.Type
}

Jump to

Keyboard shortcuts

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