Documentation
¶
Overview ¶
Package graft provides a graph-based dependency execution framework.
Graft allows you to define nodes that declare their dependencies explicitly, and executes them in topological order with automatic parallelization. Nodes at the same level (no interdependencies) run concurrently.
Quick Start ¶
Define nodes with typed Run functions:
graft.Register(graft.Node[Config]{
ID: "config",
DependsOn: []graft.ID{},
Run: func(ctx context.Context) (Config, error) {
return Config{Host: "localhost"}, nil
},
})
graft.Register(graft.Node[*sql.DB]{
ID: "db",
DependsOn: []graft.ID{"config"},
Run: func(ctx context.Context) (*sql.DB, error) {
cfg, err := graft.Dep[Config](ctx)
if err != nil {
return nil, err
}
return connectDB(cfg), nil
},
})
Execute for a specific node (returns typed output):
db, _, err := graft.ExecuteFor[*sql.DB](ctx)
if err != nil {
log.Fatal(err)
}
// db is already typed as *sql.DB
Type Safety ¶
The generic Node[T] type ensures compile-time type checking on Run return values. The type parameter T specifies what the node produces.
Dependency Access ¶
Use Dep to retrieve dependency outputs with type safety:
cfg, err := graft.Dep[config.Output](ctx)
Subgraph Execution ¶
Use ExecuteFor to execute a specific node and its transitive dependencies:
appOut, results, err := graft.ExecuteFor[app.Output](ctx) // appOut is typed; results map available for accessing other outputs
Static Analysis ¶
Use AssertDepsValid in tests to verify dependency declarations:
func TestDeps(t *testing.T) {
graft.AssertDepsValid(t, ".")
}
Index ¶
- Variables
- func AnalyzeDir(dir string) ([]typeaware.Result, error)
- func AssertDepsValid(t testing.TB, dir string, opts ...AssertOption)
- func CheckDepsValid(dir string) ([]typeaware.Result, error)
- func Dep[T any](ctx context.Context) (T, error)
- func Execute(ctx context.Context, opts ...Option) (map[ID]any, error)
- func ExecuteFor[T any](ctx context.Context, opts ...Option) (T, results, error)
- func PrintGraph(w io.Writer, opts ...Option) error
- func PrintMermaid(w io.Writer, opts ...Option) error
- func Register[T any](n Node[T])
- func Registry() map[ID]node
- func ResetDefaultCache()
- func ResetRegistry()
- func Result[T any](r results) (T, error)
- func ValidateDeps(dir string) error
- type AnalysisResult
- type AssertOption
- type AssertOpts
- type Cache
- type ID
- type MemoryCache
- type Node
- type Option
Constants ¶
This section is empty.
Variables ¶
var AnalyzeDirDebug = false
AnalyzeDirDebug controls whether AnalyzeDir prints debug information. Set this to true before calling AssertDepsValidVerbose to see file-level tracing.
Functions ¶
func AnalyzeDir ¶
AnalyzeDir analyzes all Go files in a directory for dependency correctness.
This function uses type-aware analysis with go/packages and go/ssa to accurately detect dependency issues. It discovers all graft.Node[T] registrations in the directory and compares declared dependencies (in DependsOn) against actual Dep[T] usage in Run functions.
The type-aware approach is more robust than AST-based pattern matching:
- Handles type aliases correctly
- Resolves package imports accurately
- Works with various code structures (dependencies in same package, etc.)
- Uses SSA for precise dataflow analysis
Returns all nodes found with their analysis results. Use AnalysisResult.HasIssues to filter for problems.
Example:
results, err := graft.AnalyzeDir("./nodes")
if err != nil {
log.Fatal(err)
}
for _, r := range results {
if r.HasIssues() {
fmt.Println(r.String())
}
}
func AssertDepsValid ¶
func AssertDepsValid(t testing.TB, dir string, opts ...AssertOption)
AssertDepsValid is a test helper that validates all graft.Node dependency declarations in the specified directory match their actual usage.
Add this to your test suite to catch dependency mismatches at test time rather than runtime. It uses AST analysis to compare DependsOn declarations against actual Dep[T] calls in Run functions.
This will fail the test if:
- Any node uses Dep[T](ctx) without declaring the corresponding dependency in DependsOn
- Any node declares a dependency in DependsOn but never uses it
Basic usage in your test file:
func TestNodeDependencies(t *testing.T) {
graft.AssertDepsValid(t, ".")
}
With verbose output (shows each node's deps):
func TestNodeDependencies(t *testing.T) {
graft.AssertDepsValid(t, ".", graft.WithVerboseTesting())
}
With AST debug output:
func TestNodeDependencies(t *testing.T) {
graft.AssertDepsValid(t, ".", graft.WithDebugTesting())
}
Example failure output:
graft.AssertDepsValid: db (nodes/db/db.go): undeclared deps: [cache] → node "db" uses Dep[cache.Output](ctx) but does not declare "cache" in DependsOn
func CheckDepsValid ¶
CheckDepsValid is like AssertDepsValid but returns results instead of failing.
This is useful for custom validation logic, reporting, or CI integration where you need programmatic access to the results.
Example:
results, err := graft.CheckDepsValid("./nodes")
if err != nil {
log.Fatal(err)
}
for _, r := range results {
if r.HasIssues() {
notify(r.NodeID, r.Undeclared, r.Unused)
}
}
func Dep ¶
Dep retrieves a dependency's output from the context with type assertion.
This is the primary way for nodes to access their dependencies' outputs. The type parameter T specifies the expected output type, and the node ID is derived from T using the type-to-ID mapping established at registration.
Returns an error if:
- The type T is not registered as a node output
- The context has no results (called outside of a node's Run function)
- The dependency is not found (not declared in DependsOn)
- The dependency's output cannot be asserted to type T
Example:
func(ctx context.Context) (MyOutput, error) {
cfg, err := graft.Dep[config.Output](ctx)
if err != nil {
return MyOutput{}, err
}
// use cfg...
}
func Execute ¶ added in v0.1.3
Execute runs all registered nodes and returns their results. Note that Execute is not type safe since it is executing the entire graph and exposing all the results. Use ExecuteFor instead to have a type safe output.
Nodes are executed in topological order with automatic parallelization. Nodes at the same dependency level run concurrently.
By default, uses the global registry. Use WithRegistry for a custom registry.
Example:
import (
_ "myapp/nodes/config"
_ "myapp/nodes/db"
)
func main() {
results, err := graft.Execute(ctx)
if err != nil {
log.Fatal(err)
}
db := results["db"].(*sql.DB)
}
func ExecuteFor ¶ added in v0.1.3
ExecuteFor runs the node that produces type T and its transitive dependencies.
The target node is determined by the type parameter T, which must match the output type used when registering the node. Returns the typed result and the full results map for accessing other node outputs if needed.
By default, uses the global registry. Use WithRegistry for a custom registry.
Returns an error if the type is not registered or execution fails.
Example:
appOut, results, err := graft.ExecuteFor[app.Output](ctx) // appOut is typed as app.Output // results map available for accessing dependencies: config, _ := graft.Result[config.Output](results)
func PrintGraph ¶ added in v0.1.6
PrintGraph outputs an ASCII representation of the dependency graph to the provided io.Writer.
func PrintMermaid ¶ added in v0.1.6
PrintMermaid outputs a Mermaid diagram of the dependency graph to the provided io.Writer.
func Register ¶
Register adds a typed node to the global registry.
The type parameter is erased internally for heterogeneous storage. This is typically called from init() functions in node packages, ensuring all nodes are registered before main() runs. This pattern allows nodes to be self-registering via blank imports.
Panics if a node with the same ID is already registered. This catches accidental ID collisions at startup.
Example:
// nodes/config/config.go
package config
type Output struct {
Host string
Port int
}
func init() {
graft.Register(graft.Node[Output]{
ID: "config",
DependsOn: []graft.ID{},
Run: loadConfig,
})
}
func loadConfig(ctx context.Context) (Output, error) {
return Output{Host: "localhost", Port: 5432}, nil
}
Then import the package for its side effects:
import _ "myapp/nodes/config"
func Registry ¶
func Registry() map[ID]node
Registry returns a copy of all registered nodes.
The returned map is a copy; modifications do not affect the global registry. This is commonly passed to WithRegistry for custom execution scenarios.
Example:
nodes := graft.Registry()
fmt.Printf("Registered %d nodes\n", len(nodes))
func ResetDefaultCache ¶ added in v0.1.3
func ResetDefaultCache()
ResetDefaultCache clears the global default cache. This is primarily useful for test isolation.
func ResetRegistry ¶ added in v0.1.3
func ResetRegistry()
ResetRegistry clears the global registry. This is primarily useful for test isolation.
func Result ¶ added in v0.1.3
Result retrieves a node's output from a results map with type assertion.
This is used after Execute/ExecuteFor to access node outputs from the returned results map. The node ID is derived from T using the type-to-ID mapping established at registration.
Returns an error if:
- The type T is not registered as a node output
- The node is not found in the results
- The output cannot be asserted to type T
Example:
results, _ := graft.Execute(ctx) cfg, err := graft.Result[config.Output](results)
func ValidateDeps ¶
ValidateDeps is a convenience function that returns an error if any dependency issues are found.
Pass "." for the current directory or a specific path. This is useful for CI integration or programmatic validation.
Example:
if err := graft.ValidateDeps("./nodes"); err != nil {
log.Fatal(err)
}
Types ¶
type AnalysisResult ¶
type AnalysisResult struct {
// NodeID is the ID field value from the analyzed node.
NodeID string
// File is the path to the source file containing the node.
File string
// DeclaredDeps are the dependency IDs listed in the DependsOn field.
DeclaredDeps []string
// UsedDeps are the dependency IDs accessed via Dep[T] calls in Run.
UsedDeps []string
// Undeclared are dependencies used but not declared in DependsOn.
// These will cause runtime errors.
Undeclared []string
// Unused are dependencies declared but never used.
// These indicate dead code or missing implementation.
Unused []string
// Cycles are circular dependency paths this node participates in.
// Each cycle is represented as a path of node IDs forming a loop.
// For example: ["svc5", "svc5-2", "svc5"] indicates svc5 → svc5-2 → svc5.
Cycles [][]string
}
AnalysisResult contains the result of analyzing a node's dependency usage.
It captures both declared dependencies (in DependsOn) and used dependencies (via Dep[T] calls), allowing detection of mismatches.
func (AnalysisResult) HasIssues ¶
func (r AnalysisResult) HasIssues() bool
HasIssues returns true if there are undeclared, unused dependencies, or cycles.
func (AnalysisResult) String ¶
func (r AnalysisResult) String() string
String returns a human-readable summary of issues.
Returns "NodeID: OK" if there are no issues, otherwise returns a summary of undeclared, unused dependencies, and cycles.
type AssertOption ¶ added in v0.2.0
type AssertOption func(*AssertOpts)
AssertOption is a functional option for configuring AssertDepsValid.
func WithDebugTesting ¶ added in v0.2.0
func WithDebugTesting() AssertOption
WithDebugTesting enables AST-level debug output showing file walking, composite literal detection, and dependency extraction details.
func WithVerboseTesting ¶ added in v0.2.0
func WithVerboseTesting() AssertOption
WithVerboseTesting enables verbose output showing each node's declared and used dependencies along with their validation status.
type AssertOpts ¶ added in v0.2.0
type AssertOpts struct {
Verbose bool // prints node summaries (DeclaredDeps, UsedDeps, Status)
Debug bool // prints AST-level tracing (file walking, composite literals, etc.)
}
AssertOpts configures the behavior of AssertDepsValid.
type Cache ¶ added in v0.1.3
type Cache interface {
// Snapshot returns a copy of the cache.
Snapshot() map[ID]any
// Get retrieves a cached value. Returns (value, true, nil) on hit,
// (nil, false, nil) on miss, or (nil, false, err) on failure.
Get(ctx context.Context, id ID) (any, bool, error)
// Set stores a value in the cache.
Set(ctx context.Context, id ID, value any) error
}
Cache defines the interface for node output caching. Implementations can be in-memory, Redis, disk, etc.
type MemoryCache ¶ added in v0.1.3
type MemoryCache struct {
// contains filtered or unexported fields
}
MemoryCache is a simple thread-safe in-memory cache.
func DefaultCache ¶ added in v0.1.3
func DefaultCache() *MemoryCache
DefaultCache returns the global cache instance. This can be used to inspect or clear cached values.
func NewMemoryCache ¶ added in v0.1.3
func NewMemoryCache() *MemoryCache
NewMemoryCache creates a new in-memory cache.
func (*MemoryCache) Clear ¶ added in v0.1.3
func (m *MemoryCache) Clear()
Clear removes all entries from the cache.
func (*MemoryCache) Delete ¶ added in v0.1.3
func (m *MemoryCache) Delete(ids ...ID)
Delete removes specific entries from the cache.
func (*MemoryCache) Snapshot ¶ added in v0.1.3
func (m *MemoryCache) Snapshot() map[ID]any
Snapshot returns a copy of all cached values (useful for debugging/inspection).
type Node ¶
type Node[T any] struct { // ID is the unique identifier for this node. // This is used to reference the node in DependsOn lists and Dep calls. ID ID // DependsOn lists the IDs of nodes that must complete before this node runs. // The engine ensures all dependencies have completed and their outputs // are available via Dep before calling Run. DependsOn []ID // Run executes the node's business logic and returns a typed output. // Dependencies are accessed via Dep[T](ctx). Run func(ctx context.Context) (T, error) // Cacheable indicates whether this node's output should be cached. // When true and a cache is provided via WithCache, the node's output // is stored after first execution and reused on subsequent runs. // Default is false (not cached). Cacheable bool }
Node represents a single node in the dependency graph with a typed output.
The type parameter T specifies the output type of the Run function, providing compile-time type safety. Each node has a unique ID, declares its dependencies, and provides a Run function that executes its business logic.
Example:
graft.Node[MyOutput]{
ID: "mynode",
DependsOn: []graft.ID{config.ID, db.ID},
Run: func(ctx context.Context) (MyOutput, error) {
cfg, _ := graft.Dep[config.Output](ctx)
db, _ := graft.Dep[db.Output](ctx)
return doWork(cfg, db.Pool), nil
},
}
type Option ¶ added in v0.1.3
type Option func(*config)
Option configures execution behavior.
func DisableCache ¶ added in v0.1.3
func DisableCache() Option
DisableCache disables the use of the default global cache.
Example:
// Disable the use of the default global cache out, _, err := graft.ExecuteFor[app.Output](ctx, graft.DisableCache())
func IgnoreCache ¶ added in v0.1.3
IgnoreCache forces re-execution of the specified cacheable nodes, bypassing any cached values. The fresh results are still written back to the cache.
This is useful for invalidating specific nodes without clearing the entire cache (e.g., to refresh config after a change).
Example:
// Re-fetch config even though it's cacheable
out, _, err := graft.ExecuteFor[app.Output](ctx,
graft.WithCache(cache),
graft.IgnoreCache("config"),
)
func MergeRegistry ¶ added in v0.1.3
MergeRegistry merges the provided registry with the global registry. On conflicts, the provided registry takes precedence.
This is useful for testing where you want to override specific nodes while keeping the rest of the registered graph.
Example:
// Override just the "db" node for testing out, _, err := graft.ExecuteFor[app.Output](ctx, graft.MergeRegistry(mockNodes))
func Patch ¶ added in v0.1.5
Patch replaces a node with a custom node for testing.
The node is identified by the type T, which must match a registered node's output type. The patched node inherits DependsOn, Run, and Cacheable from the provided Node[T].
This is a no-op if type T is not registered.
Example:
// Replace db node with a mock that uses config
out, _, err := graft.ExecuteFor[app.Output](ctx,
graft.Patch[db.Output](graft.Node[db.Output]{
DependsOn: []graft.ID{"config"},
Run: func(ctx context.Context) (db.Output, error) {
cfg, _ := graft.Dep[config.Output](ctx)
return db.Output{Pool: mockPool(cfg)}, nil
},
}),
)
func PatchValue ¶ added in v0.1.5
PatchValue replaces a node's output with a fixed value for testing.
The node is identified by the type T, which must match a registered node's output type. The patched node has no dependencies and simply returns the provided value.
This is a no-op if type T is not registered.
Example:
// Replace config node with a test value
results, err := graft.Execute(ctx,
graft.PatchValue[config.Output](config.Output{Host: "test", Port: 9999}),
)
func WithCache ¶ added in v0.1.3
WithCache overrides the default global cache with a custom cache.
By default, Execute/ExecuteFor use a global in-memory cache (similar to the global registry), so caching works automatically across calls. Use WithCache to provide a custom cache implementation (e.g., Redis) or an isolated cache for testing.
Example:
// Use a custom cache instead of the global default customCache := graft.NewMemoryCache() out, _, err := graft.ExecuteFor[app.Output](ctx, graft.WithCache(customCache))
func WithRegistry ¶ added in v0.1.3
WithRegistry uses a custom node registry instead of the global registry.
Example:
out, _, err := graft.ExecuteFor[app.Output](ctx, graft.WithRegistry(customNodes))
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
complex
command
|
|
|
diamond
command
|
|
|
edgecases/aliased_import
command
|
|
|
fanout
command
|
|
|
httpserver
command
Package main demonstrates using graft with node-level caching for HTTP servers.
|
Package main demonstrates using graft with node-level caching for HTTP servers. |
|
simple
command
|
|
|
internal
|
|