# GenDI > GenDI — Generated Dependency Injection. A compile-time DI container generator for Go, configured in YAML. Source: https://gendi.dev/ --- # Getting Started Source: https://gendi.dev/docs/ Everything GenDI does happens before your program runs. It reads a YAML file, resolves every dependency against real Go types, and writes one Go file containing the container your `main` would otherwise assemble by hand. Nothing is reflected at runtime and nothing is autowired at generation time: what you declare is what you get. This page walks the whole loop once — from an empty project to a running program. Every step links to the reference page that covers it in full. ## Install GenDI is a generator, so it belongs in your module as a tool dependency: ```bash go get -tool github.com/gendi-org/gendi/cmd/gendi ``` That records it in `go.mod` and makes it runnable as `go tool gendi`, pinned to a version like any other dependency. ## Start from real Go code GenDI wires constructors that already exist; it never writes the services themselves. A constructor is any function returning `T` or `(T, error)`: ```go // greet/greet.go package greet type Greeter struct { prefix string } func New(prefix string) *Greeter { return &Greeter{prefix: prefix} } func (g *Greeter) Greet(name string) string { return g.prefix + ", " + name + "!" } ``` ## Declare one service ```yaml # gendi.yaml services: greeter: constructor: func: "example.com/myapp/greet.New" args: - "Hello" public: true ``` `func` is a full package path followed by the function name — GenDI loads that package and type-checks the call. `public: true` asks for a getter; without it there is no way into the service from your code. Generate: ```bash go tool gendi --config=gendi.yaml --out=./di --pkg=di ``` A successful run prints nothing. `di/container_gen.go` now ends with: ```go func (c *Container) buildGreeter() (*greet.Greeter, error) { return greet.New("Hello"), nil } func (c *Container) getGreeter() (*greet.Greeter, error) { if c.svc_greeter != nil { return c.svc_greeter, nil } res, err := c.buildGreeter() if err != nil { return nil, err } c.svc_greeter = res return res, nil } func (c *Container) GetGreeter() (*greet.Greeter, error) { c.mu.Lock() defer c.mu.Unlock() return c.getGreeter() } func (c *Container) MustGreeter() *greet.Greeter { res, err := c.GetGreeter() if err != nil { c.onMustCallFailed("greeter", err) panic(err) } return res } ``` That is the whole pattern: a `build` function that calls your constructor, a lock-free internal getter that caches, and a locking public getter in two flavours. See [Services](configuration/services.md) for every field a service definition accepts. ## Use it ```go // main.go package main import ( "fmt" "example.com/myapp/di" ) func main() { fmt.Println(di.NewContainer(nil).MustGreeter().Greet("world")) } ``` ``` Hello, world! ``` `NewContainer(nil)` falls back to the parameter defaults compiled into the container. Every public service gets both `GetX() (T, error)` and `MustX() T`; `MustX` panics and additionally reports through the container error handler if one was installed. ## Inject a dependency A service reference is an argument prefixed with `@`: ```yaml services: greeter: constructor: func: "example.com/myapp/greet.New" args: - "Hello" server: constructor: func: "example.com/myapp/app.NewServer" args: - "@greeter" public: true ``` `greeter` is no longer public, and two things follow from that in the regenerated file: - there is no `GetGreeter`/`MustGreeter` any more. Public services are the roots of the container; a non-public service is reachable only from Go code GenDI itself generates — see [Aliases and Visibility](configuration/visibility.md) - `greeter` also lost its cache field, even though services are shared by default. With exactly one holder, a cache is unobservable, so the generator drops it — see [Service Lifecycle](configuration/lifecycle.md) Neither is a special case you have to configure. Declaring the dependency was the whole change; ordering, construction and error propagation are derived. ## Move a value out of the config An argument in `%percent%` form is a parameter reference: ```yaml parameters: greeting: "Hello" services: greeter: constructor: func: "example.com/myapp/greet.New" args: - "%greeting%" # ... ``` The YAML value is only a default. It is emitted as a package-level provider: ```go var DefaultContainerParameters = parameters.NewProviderMap(map[string]any{ "greeting": "Hello", }) ``` and the constructor argument now goes through the resolver: ```go func (c *Container) buildGreeter() (*greet.Greeter, error) { var zero *greet.Greeter param0_greeting, err := c.paramsResolver.String("greeting") if err != nil { return zero, fmt.Errorf("service %q arg[%d] param %q: %w", "greeter", 0, "greeting", err) } return greet.New(param0_greeting), nil } ``` Note which method was emitted: `String`. Parameters have no declared type — the target type comes from the constructor argument the parameter is injected into, and the typed call is chosen at generation time. To supply real values, hand the container a provider instead of `nil`: ```go custom := di.NewContainer(parameters.NewProviderMap(map[string]any{ "greeting": "Hola", })) fmt.Println(custom.MustServer().Handle("world")) ``` ``` Hello, world! Hola, world! ``` A provider can be a map, your own config struct tagged with `di-param`, or a composite of several sources — see [Parameters](configuration/parameters.md). ## Keep it in sync Put the command next to the package it generates and let `go generate` run it: ```go //go:generate go tool gendi --config=gendi.yaml --out=./di --pkg=di ``` Two runs on the same configuration produce byte-identical output, so the generated file is safe to commit and reviews as an ordinary diff. Regenerating is how you find out that a constructor signature changed: the mismatch becomes a generation error rather than a runtime surprise. ## Where to go next - [Configuration Reference](configuration/_index.md) — every YAML key, one page per concept - [Compiler Passes](passes.md) — rewriting the configuration before generation - [Troubleshooting](troubleshooting.md) — what the generation errors mean - [Design](design.md) — what the container guarantees, and what GenDI deliberately does not do --- # CLI Source: https://gendi.dev/docs/cli/ ```bash go tool gendi [flags] ``` | Flag | Description | | |------|-------------|---| | `--config string` | Root YAML configuration file | required | | `--out string` | Output directory or file | required | | `--pkg string` | Go package name | required | | `--container string` | Container struct name | default `Container` | | `--build-tags string` | Go build tags | used for type resolution *and* emitted as the generated file's `//go:build` header | | `--enable-pass value` | Enable a specific compiler pass (can be specified multiple times) | | | `--verbose` | Verbose logging | prints the path of the file that was written | A successful run is otherwise silent. ## Selectable Passes The stock binary ships two selectable passes; an unknown name is an error raised before generation starts: | `--enable-pass` | Effect | |-----------------|--------| | `slog` | Gives every service tagged `slog` with a `channel` attribute its own channel-scoped logger derived from the `logger` service (see [stdlib/README.md](https://github.com/gendi-org/gendi/blob/master/stdlib/README.md#slogpass)) | | `expose-all` | Marks every service public, so each one gets a getter. For test containers — it overrides explicit `public: false` and disables unreachable-service pruning | Registering passes of your own means building your own generator binary — see [Building a Custom Generator](embedding.md). ## Examples ```bash # Generate into a directory go tool gendi --config=gendi.yaml --out=./di --pkg=di # Generate a specific file go tool gendi --config=gendi.yaml --out=./di/container_gen.go --pkg=di # Resolve types under a build tag, and emit it as the file's //go:build header go tool gendi --config=gendi.yaml --out=./di --pkg=di --build-tags=integration ``` Keeping the command next to the package it writes is what makes regeneration routine: ```go //go:generate go tool gendi --config=gendi.yaml --out=./di --pkg=di ``` --- # Shipping Wiring in a Library Source: https://gendi.dev/docs/library-wiring/ A library that wants to be easy to wire usually settles for exposing constructors and a paragraph of README explaining the order to call them in. The alternative most DI frameworks offer is worse: publish a module object, and now every consumer of the library depends on the framework, whether they use it or not. GenDI lets a library ship its wiring as a file. The library's Go API keeps no trace of it, its dependency graph does not grow, and a consumer who never runs GenDI is unaffected. ## The library side A config sits next to the code it wires: ``` greetlib/ ├── go.mod ├── greet.go └── gendi.yaml ``` Nothing is added to `go.mod` — the wiring is data, not a dependency: ``` module example.com/greetlib go 1.25.4 ``` ```yaml # gendi.yaml parameters: greetlib.greeting: "Hello" services: greetlib.greeter: constructor: func: "$this.New" args: - "%greetlib.greeting%" ``` `$this` is the Go package path of the directory the config lives in, so the file keeps working whoever imports it and wherever the module is checked out. ## The consumer side Depend on the library the ordinary way, then import its config by module path: ```yaml # gendi.yaml imports: - example.com/greetlib/gendi.yaml parameters: greetlib.greeting: "Hola" services: greeter: alias: "greetlib.greeter" public: true ``` The module is located through the consumer's `go.mod` graph, `replace` directives included. See [Imports](configuration/imports.md) for the resolution rules and the sandboxing that goes with them. Generating produces a container that calls the library directly: ```go var DefaultContainerParameters = parameters.NewProviderMap(map[string]any{ "greetlib.greeting": "Hola", }) ``` ```go func (c *Container) buildGreetlibGreeter() (*greetlib.Greeter, error) { var zero *greetlib.Greeter param0_greetlib_greeting, err := c.paramsResolver.String("greetlib.greeting") if err != nil { return zero, fmt.Errorf("service %q arg[%d] param %q: %w", "greetlib.greeter", 0, "greetlib.greeting", err) } return greetlib.New(param0_greetlib_greeting), nil } ``` ``` Hola, world! ``` Two things to notice. The consumer's `greetlib.greeting` replaced the library's default — a library's parameters are defaults, and the importing file is merged last. And the generated code imports `example.com/greetlib` and calls its constructor: nothing about the wiring survives into the running program. ## Conventions ### Namespace every ID and parameter Service IDs and parameter names share one flat space across every imported file, and later definitions win silently. Prefix both with the library name — `greetlib.greeter`, `greetlib.greeting` — the way [`stdlib/gendi.yaml`](https://github.com/gendi-org/gendi/blob/master/stdlib/gendi.yaml) does with `stdlib.`. Without a prefix, two libraries that both declare `logger` will quietly overwrite each other in whichever order they were imported. ### Do not mark anything public Visibility is the application's decision. `public: true` is also a pruning root, so a public service in a library is emitted into *every* consumer's container and can never be pruned away: ```go func (c *Container) GetGreetlibShouty() (*greetlib.Greeter, error) func (c *Container) MustGreetlibShouty() *greetlib.Greeter ``` Leave the services internal and let the consumer alias the ones it wants, as above. See [Aliases and Visibility](configuration/visibility.md). ### Use explicit package paths outside the config's own directory `$this` follows the directory of the file it appears in, not the module root. Splitting a library config into `services/*.yaml` and writing `$this.New` there makes GenDI look for a Go package that does not exist: ``` generate: -: no required module provides package example.com/greetlib/services; to add it: go get example.com/greetlib/services ``` In a split config, write the package path out in full — which is what `stdlib/gendi.yaml` does. ### What the consumer needs - the library as a normal module dependency, so the import resolves through `go.mod` - the `github.com/gendi-org/gendi` module itself, if any config in the graph declares a tag — see [Troubleshooting](troubleshooting.md#no-required-module-provides-package-githubcomgendi-orggendistdlib) ## What this is not It is not a plugin mechanism. The config is read at generation time by the consumer's own run of GenDI; the library never executes and never registers anything. That is the point: the file is inert until someone chooses to generate against it. --- # Compiler Passes Source: https://gendi.dev/docs/passes/ Compiler passes transform configuration before code generation, enabling project-specific conventions and patterns. ## Overview Compiler passes allow you to programmatically modify the DI configuration before container generation. This enables: - **Auto-tagging** by naming conventions - **Service registration** from code analysis - **Argument transformation** (e.g., adding logging) - **Validation** of custom constraints - **Configuration normalization** ### When to Use Passes Use compiler passes when you need: - Project-specific conventions (e.g., auto-tag all `*Handler` services) - Dynamic service registration from external sources - Custom validation rules beyond type checking - Configuration preprocessing (e.g., environment variable expansion) - Code generation from annotations ### When NOT to Use Passes Don't use passes for: - Simple configuration changes (use YAML imports/overrides instead) - Type transformations (use decorators instead) - Runtime behavior modification (implement in your services) ## Pass Interface All compiler passes implement the `Pass` interface: ```go package di type Pass interface { Name() string Process(cfg *Config) (*Config, error) } ``` ### Interface Methods **`Name() string`** - Returns a unique identifier for the pass - Used in error messages and logging - Should be lowercase with hyphens (e.g., `"auto-tag"`) **`Process(cfg *Config) (*Config, error)`** - Receives the current configuration - Returns the transformed configuration - Returns an error if transformation fails - **Mutates the config in place** and returns it for chaining ## Creating a Pass ### Basic Pass Structure ```go package passes import ( di "github.com/gendi-org/gendi" ) type MyPass struct { // Optional: configuration fields } func (p *MyPass) Name() string { return "my-pass" } func (p *MyPass) Process(cfg *di.Config) (*di.Config, error) { // Transform cfg // Return modified config or error return cfg, nil } ``` The worked examples below fill this skeleton in — see [Auto-Tagging by Convention](#auto-tagging-by-convention) for a complete pass. ### Modifying the Configuration The `Config` struct contains three maps: ```go type Config struct { Parameters map[string]Parameter Tags map[string]Tag Services map[string]Service } ``` **Safe modification pattern:** ```go func (p *MyPass) Process(cfg *di.Config) (*di.Config, error) { // Iterate over services for id, svc := range cfg.Services { // Modify service svc.Tags = append(svc.Tags, di.ServiceTag{ Name: "my-tag", }) // Write back to map (services are value types) cfg.Services[id] = svc } return cfg, nil } ``` **Important:** Services, parameters, and tags are value types. Always write back to the map after modification. ### Adding Services ```go func (p *MyPass) Process(cfg *di.Config) (*di.Config, error) { cfg.Services["new_service"] = di.Service{ Constructor: di.Constructor{ Func: "github.com/myapp.NewService", Args: []di.Argument{ {Kind: di.ArgLiteral, Literal: di.NewStringLiteral("value")}, }, }, Shared: true, } return cfg, nil } ``` ### Adding Tags ```go func (p *MyPass) Process(cfg *di.Config) (*di.Config, error) { cfg.Tags["my-tag"] = di.Tag{ ElementType: "github.com/myapp.Handler", SortBy: "priority", } return cfg, nil } ``` ### Adding Parameters ```go func (p *MyPass) Process(cfg *di.Config) (*di.Config, error) { cfg.Parameters["new_param"] = di.Parameter{ Value: di.NewStringLiteral("default-value"), } return cfg, nil } ``` ## Common Use Cases ### Auto-Tagging by Convention Tag services based on naming patterns: ```go type AutoTagPass struct{} func (p *AutoTagPass) Name() string { return "auto-tag" } func (p *AutoTagPass) Process(cfg *di.Config) (*di.Config, error) { for id, svc := range cfg.Services { // Tag all services ending with ".handler" if strings.HasSuffix(id, ".handler") { svc.Tags = append(svc.Tags, di.ServiceTag{ Name: "http.handler", }) cfg.Services[id] = svc } // Tag all services starting with "middleware." if strings.HasPrefix(id, "middleware.") { svc.Tags = append(svc.Tags, di.ServiceTag{ Name: "http.middleware", }) cfg.Services[id] = svc } } return cfg, nil } ``` ### Adding Logging to Services Automatically inject logger into all services: ```go type LoggingPass struct{} func (p *LoggingPass) Name() string { return "auto-logging" } func (p *LoggingPass) Process(cfg *di.Config) (*di.Config, error) { for id, svc := range cfg.Services { // Skip logger itself if id == "logger" { continue } // Add logger as first argument svc.Constructor.Args = append( []di.Argument{{Kind: di.ArgServiceRef, Value: "logger"}}, svc.Constructor.Args..., ) cfg.Services[id] = svc } return cfg, nil } ``` ### Custom Validation Validate custom business rules: ```go type ValidationPass struct{} func (p *ValidationPass) Name() string { return "validation" } func (p *ValidationPass) Process(cfg *di.Config) (*di.Config, error) { // Ensure all public services are shared for id, svc := range cfg.Services { if svc.Public && !svc.Shared { return nil, fmt.Errorf( "service %q is public but not shared", id, ) } } // Ensure required services exist requiredServices := []string{"logger", "database"} for _, required := range requiredServices { if _, exists := cfg.Services[required]; !exists { return nil, fmt.Errorf( "required service %q not found", required, ) } } return cfg, nil } ``` ### Priority-Based Ordering Automatically assign priorities based on registration order: ```go type PriorityPass struct{} func (p *PriorityPass) Name() string { return "auto-priority" } func (p *PriorityPass) Process(cfg *di.Config) (*di.Config, error) { priority := 1000 for id, svc := range cfg.Services { // Add priority to all handler tags for i, tag := range svc.Tags { if tag.Name == "handler" && tag.Attributes["priority"] == nil { if svc.Tags[i].Attributes == nil { svc.Tags[i].Attributes = make(map[string]interface{}) } svc.Tags[i].Attributes["priority"] = priority priority += 10 } } cfg.Services[id] = svc } return cfg, nil } ``` ### Environment-Based Configuration Load configuration from environment: ```go type EnvPass struct{} func (p *EnvPass) Name() string { return "env-config" } func (p *EnvPass) Process(cfg *di.Config) (*di.Config, error) { // Override parameters from environment for name, param := range cfg.Parameters { envKey := "APP_" + strings.ToUpper( strings.ReplaceAll(name, ".", "_"), ) if envValue := os.Getenv(envKey); envValue != "" { param.Value = di.NewStringLiteral(envValue) cfg.Parameters[name] = param } } return cfg, nil } ``` ## Best Practices ### 1. Keep Passes Focused Each pass should do one thing well: ```go // ✅ Good: Focused pass type AutoTagHandlerPass struct{} // ❌ Bad: Does too much type AutoConfigureEverythingPass struct{} ``` ### 2. Document Pass Behavior Add clear documentation: ```go // AutoTagPass automatically tags services by naming convention: // - Services ending with ".handler" → "http.handler" tag // - Services starting with "middleware." → "http.middleware" tag type AutoTagPass struct{} ``` ### 3. Fail Fast with Clear Errors Return descriptive errors: ```go if svc.Constructor.Func == "" && svc.Constructor.Method == "" { return nil, fmt.Errorf( "service %q: missing constructor (required by auto-tag pass)", id, ) } ``` ### 4. Don't Assume Configuration State Validate your assumptions: ```go func (p *MyPass) Process(cfg *di.Config) (*di.Config, error) { if cfg.Services == nil { cfg.Services = make(map[string]di.Service) } // Now safe to modify cfg.Services // ... } ``` ### 5. Use Source Locations Add source location tracking for better error messages: ```go svc.Constructor.SourceLoc = &srcloc.Location{ File: "auto-generated", Line: 0, } ``` ### 6. Test Your Passes Write unit tests: ```go func TestAutoTagPass(t *testing.T) { cfg := &di.Config{ Services: map[string]di.Service{ "home.handler": { Constructor: di.Constructor{ Func: "app.NewHomeHandler", }, }, }, } pass := &AutoTagPass{} result, err := pass.Process(cfg) if err != nil { t.Fatalf("unexpected error: %v", err) } svc := result.Services["home.handler"] if len(svc.Tags) != 1 || svc.Tags[0].Name != "http.handler" { t.Errorf("expected http.handler tag, got: %v", svc.Tags) } } ``` ### 7. Order Matters Passes run in sequence. Be aware of dependencies: ```go customPasses := []di.Pass{ &NormalizationPass{}, // Run first: normalize config &AutoTagPass{}, // Then: add tags &ValidationPass{}, // Finally: validate result } ``` If these passes are registered with `cmd.Run`, use `[]di.Pass`. ## API Reference The types a pass works with — `Config`, `Service`, `Constructor`, `Argument`/`ArgumentKind`, `Tag`, `ServiceTag`, `Parameter` — and the `NewStringLiteral`/`NewIntLiteral`/`NewFloatLiteral`/`NewBoolLiteral`/`NewNullLiteral` constructors are declared in `config.go` and `literal.go` of the root `di` package. Read them there or on [pkg.go.dev](https://pkg.go.dev/github.com/gendi-org/gendi#Config) rather than from a copy that can drift. Two properties worth knowing before writing a pass: - Every config struct carries an optional `SourceLoc *srcloc.Location`. Keep it when you rewrite an entry — it is what puts the offending YAML line into generation errors - `Packages` fields are derived, not authored: the pipeline recomputes them after passes run, so a pass never has to maintain them ## See Also - [Configuration Reference](./configuration/_index.md) - [Custom Pass Example](https://github.com/gendi-org/gendi-example-app) (`tools/gendi/`) - [Design](./design.md) - [API Documentation](https://pkg.go.dev/github.com/gendi-org/gendi) --- # Troubleshooting Source: https://gendi.dev/docs/troubleshooting/ GenDI reports everything it can before your program is built, so most problems arrive as a generation error rather than a runtime failure. This page collects the ones you are most likely to meet and what each of them means. Every message below is copied from a real run, with the project directory shortened to `/path/to/myapp`. ## How to read a generation error ``` generate: phase *ir.constructorResolverPhase apply: /path/to/myapp/gendi.yaml:6:11: service "server" arg[0]: unknown service "greeter" 2 | server: 3 | constructor: 4 | func: "example.com/myapp/app.NewServer" 5 | args: 6 | - "@greeter" ^ 7 | public: true ``` Four parts, in order: the phase that rejected the configuration, the exact position in the YAML file, what was wrong and where in the service definition (`arg[0]`, `constructor.func`, …), and the offending lines with a caret under the token. The phase name is an implementation detail — it tells you *when* the check ran, not what to fix. Read from the file position onward. Some errors have no position because they are properties of the graph rather than of a single node; a dependency cycle is the usual one. ## unknown service "x" The argument references a service ID that no loaded configuration declares. Check for a typo, and check that the file declaring it was actually loaded: a glob that matches nothing under an existing directory is a silent no-op, so a mistyped import path leaves its services undeclared without complaining. See [Imports](configuration/imports.md). ## cannot use string literal "oops" as \*example.com/myapp/greet.Greeter ``` generate: phase *ir.constructorResolverPhase apply: /path/to/myapp/gendi.yaml:6:11: service "server" arg[0]: cannot use string literal "oops" as *example.com/myapp/greet.Greeter ``` The argument's type does not match the constructor parameter at that position. The most common cause is a missing `@`: without it, `greeter` is a plain string literal rather than a service reference. See [Arguments](configuration/arguments.md) for what each prefix means. ## symbol NewGreeter not found in example.com/myapp/greet ``` generate: phase *ir.constructorResolverPhase apply: /path/to/myapp/gendi.yaml:4:13: service "greeter" constructor.func: symbol NewGreeter not found in example.com/myapp/greet ``` The package loaded, but it has no such exported function. If the symbol does exist, check that it is exported, and that the file declaring it is not behind a build tag the generator was not given — `--build-tags` is used for type resolution as well as for the generated file's header. An unresolved `$this` surfaces as this same message — see [Arguments](configuration/arguments.md) for how it is expanded. ## constructor must not return the empty interface (any) ``` generate: phase *ir.constructorResolverPhase apply: /path/to/myapp/gendi.yaml:4:13: service "thing" constructor.func: constructor must not return the empty interface (any); a service needs a type the container can check statically ``` A service whose type is `any` makes every assignment to it valid, which leaves the generator nothing to verify — so it is rejected rather than generated. Return a concrete type or an interface with methods. ## circular dependency: a -> b -> a ``` generate: phase *ir.validatorPhase apply: circular dependency: a -> b -> a ``` The trace lists the whole cycle. Since the container builds every dependency before its holder, a cycle has no valid construction order, and GenDI has no lazy service or setter injection to hide one — the cycle has to go. The usual fixes are to extract what both services share into a third service they both depend on, or to decouple them at the type level: pass the data one side actually needs instead of the whole service, or let them communicate over a shared channel built by `stdlib.NewChan`. ## The getter I expected is not in the generated file Not an error, and not a bug. Only public services get `GetX`/`MustX`, and only services reachable from a public one are emitted at all. A service that is neither public nor reachable is pruned after validation. Mark it `public: true`, reach it from something public, or generate with `--enable-pass=expose-all` for a test container. See [Aliases and Visibility](configuration/visibility.md). ## parameter "x": parameter not found ``` service "greeter" arg[0] param "nope": parameter "nope": parameter not found ``` This one is a *runtime* error, and it is the intended behaviour: generation does not require a parameter to be declared in YAML, because the values a container runs with come from its provider. A name that no provider answers therefore cannot be caught before the program runs. Declare a default under `parameters:` if the container should work with `NewContainer(nil)`, or make sure the provider you pass covers the name. See [Parameters](configuration/parameters.md). ## out is required ``` config finalize: out is required ``` `--config`, `--out` and `--pkg` are all required — see [CLI](cli.md). ## no required module provides package github.com/gendi-org/gendi/stdlib ``` generate: -: no required module provides package github.com/gendi-org/gendi/stdlib; to add it: go get github.com/gendi-org/gendi/stdlib ``` A configuration that declares any tag needs the `github.com/gendi-org/gendi` module to be resolvable *from the module being generated into*, because tagged collections are desugared to `stdlib.MakeSlice` during analysis. Installing GenDI as a tool dependency satisfies this; running a prebuilt binary against a module that does not require GenDI does not. The message has no source location (`-:`) because the failure is in package loading rather than in any one line of the configuration. ## unknown pass "nope" ``` resolve passes: --enable-pass: unknown pass "nope" ``` Pass names are validated before generation starts. The stock binary ships `slog` and `expose-all`; anything else has to be registered by a generator you built yourself — see [Building a Custom Generator](embedding.md). --- # Building a Custom Generator Source: https://gendi.dev/docs/embedding/ Passes only run inside a generator binary that registers them. The stock `gendi` binary registers the built-in ones; to add your own, build a generator of your own around `cmd.Run`. ## Building a Custom Generator To use custom passes, create a custom generator binary: ### 1. Create Generator Package **tools/gendi/main.go:** ```go package main import ( "flag" "fmt" "os" di "github.com/gendi-org/gendi" "github.com/gendi-org/gendi/cmd" "github.com/myapp/internal/passes" ) func main() { // Define always-included custom passes customPasses := []di.Pass{ &passes.AutoTagPass{}, } // Define selectable passes (filtered by --enable-pass flag) selectablePasses := []di.Pass{ &passes.ValidationPass{}, } // Run gendi with custom passes if err := cmd.Run(flag.CommandLine, customPasses, selectablePasses); err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(1) } } ``` ### 2. Run Custom Generator ```bash # Build custom generator go build -o bin/gendi ./tools/gendi # Run custom generator (AutoTagPass always runs) ./bin/gendi --config=gendi.yaml --out=./di --pkg=di # Enable ValidationPass via flag ./bin/gendi --config=gendi.yaml --out=./di --pkg=di --enable-pass=validation # Or use go run go run ./tools/gendi --config=gendi.yaml --out=./di --pkg=di ``` ### 3. Integrate with go:generate ```go //go:generate go run ./tools/gendi --config=gendi.yaml --out=./di --pkg=di ``` ## CLI Passes Custom generator binaries built with `cmd.Run` or `cmd.MustRun` register two types of passes: - **Always-included passes**: Passed as the first `passes` parameter, always run - **Selectable passes**: Passed as the second `selectablePasses` parameter, filtered by `--enable-pass` flag Pass names come from `Name()`. If the same pass name is registered more than once, only the first included pass runs. `cmd.BuiltinSelectablePasses()` returns the set the stock `gendi` binary registers — `stdlib.SLogPass` (`slog`) and `di.ExposeAllPass` (`expose-all`); include it in your own selectable list to keep those available: ```go selectablePasses := append(cmd.BuiltinSelectablePasses(), &passes.ValidationPass{}) ``` `cmd.Run` validates pass flags before generation and returns an error if a name passed to `--enable-pass` does not match any registered selectable pass. Use `di.Pass` when calling `di.ApplyPasses`, `cmd.Generate`, `cmd.Run`, or `cmd.MustRun`. ## Complete Example See [gendi-example-app](https://github.com/gendi-org/gendi-example-app) (`tools/gendi/`) for a production-ready implementation featuring: ### Custom Pass: Channel Logger Automatically adds structured logging with channel names to method constructors: ```go type ChannelLoggerPass struct{} func (p *ChannelLoggerPass) Name() string { return "channel-logger" } func (p *ChannelLoggerPass) Process(cfg *di.Config) (*di.Config, error) { for id, svc := range cfg.Services { // Only process method constructors if svc.Constructor.Method == "" { continue } // Extract service name from ID (e.g., "log.database" → "database") parts := strings.Split(id, ".") if len(parts) < 2 { continue } channel := parts[len(parts)-1] // Add channel and channel name as variadic arguments svc.Constructor.Args = append( svc.Constructor.Args, di.Argument{ Kind: di.ArgLiteral, Literal: di.NewStringLiteral("channel"), }, di.Argument{ Kind: di.ArgLiteral, Literal: di.NewStringLiteral(channel), }, ) cfg.Services[id] = svc } return cfg, nil } ``` ### Running the Example Clone [gendi-example-app](https://github.com/gendi-org/gendi-example-app), then: ```bash # Run custom generator go run ./tools/gendi --config=./cmd/gendi.yaml --out=./cmd --pkg=main # Run the application go run ./cmd ``` ### Example Output ``` Config before pass: Services: 2 log.database: method constructor with 0 args log.auth: method constructor with 0 args Config after pass: Services: 2 log.database: method constructor with 2 args log.auth: method constructor with 2 args Generated container with 2 services ``` --- # Design Source: https://gendi.dev/docs/design/ Why GenDI is shaped the way it is, and what the generated container looks like. For the YAML surface see the [Configuration Reference](./configuration/_index.md). ## Purpose and Scope GenDI is a Go library and code generator that produces a statically typed DI container from declarative YAML configuration. Four goals shape every decision that follows. **Nobody should assemble a service graph by hand.** Wiring written as Go grows into a `main` that is long, order-sensitive and touched by every feature — a file where the interesting change is one line among fifty of plumbing. Moving the graph into configuration leaves the plumbing to the generator and keeps the diff of a new service down to the service. **A library should be able to ship its wiring without shipping a framework.** A package can publish its own `gendi.yaml`, and a consumer imports it by module path (see [Shipping Wiring in a Library](library-wiring.md)); `stdlib/gendi.yaml` in this repository is that mechanism used on itself. The wiring travels as data, so the library's Go API stays free of container types and its dependency graph does not grow — a consumer that never uses GenDI is unaffected by the file's existence. **The generated container must be boring to read.** One build function per service, direct calls, concrete types, no reflection and no indirection to follow. Answering "what is injected here" is reading, not reasoning: the call is written out. That is what makes the output reviewable in a diff, debuggable in a stack trace, and unambiguous to any tool that reads it — including the one that wrote the configuration. **The configuration must be writable by machines, not only by people.** The surface is a small declarative file with a published JSON schema (`gendi.schema.json`), not an API to be discovered by exploration, and `site/content/docs/llm.md` states its rules in short form. What makes it safe to generate is the other end: a wrong guess fails at generation with the offending line and a caret under the token, instead of compiling into something subtly wrong. In scope: YAML service configuration, container code generation, strict static type validation, decorators and decorator chains, tagged services, configuration imports, `go generate` workflows. Out of scope: autowiring, runtime DI / service locator, reflection-based resolution, an expression language or env interpolation, hot-reload of configuration. ## Fixed Decisions 1. `services.type` is optional — inferred from the constructor, a contract when given 2. Tagged injection produces `[]T` only 3. `tags.element_type` is optional, required when `public` or `autoconfigure` is set 4. Strict typing: a service type is never `any` 5. Errors are detected at generation time ## Terminology | Term | Definition | |-----|------------| | Service ID | String identifier of a service in configuration | | Constructor | Go function or method that creates a service instance | | Service Type | Type returned by the service getter | | Shared service | Singleton service | | Non-shared service | Factory service | | Decorator | Service that wraps another service | | Tag | Label assigned to a service | | Tagged injection | Injection of a collection of services by tag | | Compiler pass | Programmatic modification of configuration before generation | ## Architecture 1. CLI generator - Parses YAML and resolves imports - Applies compiler passes - Analyzes Go code (`go/packages`) - Generates container code 2. Runtime packages - `parameters` — the only package generated containers depend on: `Provider` (lookup), `Caster`/`StandardCaster` (conversion), the `Resolver` facade over both, the shipped providers (map, struct tag, composite, null), and the `ErrParameterNotFound`/`ErrCannotCast` sentinels - `stdlib` — optional factories for common standard library types, plus `MakeSlice`/`NewChan`, which the generator inlines rather than calls 3. Generated container - `Container` struct with typed getter methods - Lazy initialization for shared services - No reflection ## Generated Container ```go type Container struct { // private fields } func NewContainer(params parameters.Provider, opts ...ContainerOption) *Container func WithContainerErrorHandler(handler func(serviceName string, err error)) ContainerOption func WithContainerParameterCaster(caster parameters.Caster) ContainerOption ``` Declared parameter defaults are emitted as a package-level provider: ```go var DefaultContainerParameters = parameters.NewProviderMap(map[string]any{ /* ... */ }) ``` It is what `NewContainer(nil)` falls back to. The map holds only the parameters a surviving service injects: unreferenced parameters are pruned, and when none remain the variable is not emitted and the fallback becomes `parameters.ProviderNullInstance`. The container stores a `parameters.Resolver` (a facade over `Provider` and `Caster`, default caster `parameters.StandardCaster`) and resolves each parameter with one typed call per injection site, e.g. `c.paramsResolver.Int("port")`. ### Getter Methods - `GetX() (T, error)` - `MustX() T` — panics on error and optionally reports via the container error handler All getters are strictly typed. Shared services are lazy singletons; non-shared services are built anew on every call. ### Reachability Pruning Public services (including desugared public tags) are the generation roots. Services unreachable from any root are removed after validation and are not emitted; the parameters only they injected are pruned with them. Pruning runs after type checking, so errors in an unreachable service still fail generation. The `expose-all` pass makes every service public and therefore leaves nothing to prune. ## Error Reporting Diagnostics carry the service ID, the configuration field or argument index, expected versus actual type, and the dependency chain where one applies: ``` service "app" arg[0]: service "cache" type *example.com/ef.Cache is not assignable to *example.com/ef.Store ``` Errors carrying a source location are rendered with the offending config line and a caret under it: ``` gendi.yaml:4:13: service "thing" constructor.func: constructor must not return the empty interface (any); a service needs a type the container can check statically 3 | constructor: 4 | func: "example.com/anytest.NewAny" ^ ``` Dependency cycles are detected at generation time; generation fails and the error includes the trace. --- # Notes for AI Agents Source: https://gendi.dev/docs/llm/ Short, stable facts about GenDI for tooling and assistants. The same text is served at `https://gendi.dev/llms.txt`, so an agent can read it in one fetch instead of crawling this documentation. The project is `GenDI`; the module, the command and the config file are all lowercase `gendi`. ## Schema `gendi.schema.json` in the repository root is the machine-readable contract for a config file — validate a generated config against it before running the generator. A test keeps it in step with the loader, so what it rejects, the loader rejects. Editors get the same checking from a first line of: ```yaml # yaml-language-server: $schema=https://raw.githubusercontent.com/gendi-org/gendi/master/gendi.schema.json ``` Anything the schema accepts can still fail generation for reasons it cannot see — unknown service IDs, type mismatches against the real Go signatures, dependency cycles. Those are reported with the offending line and a caret. ## CLI ```bash go get -tool github.com/gendi-org/gendi/cmd/gendi # once, per module go tool gendi --config=gendi.yaml --out=./di --pkg=di ``` Flags: `--config`, `--out`, `--pkg`, `--container`, `--build-tags`, `--enable-pass`, `--verbose`. ## YAML Syntax Constructor args: - `@service.id` service reference - `@.inner` inner service (decorators only) - `%param.name%` parameter - `!tagged:tag.name` tagged injection - `!spread:@service` spread slice into variadic - `!spread:!tagged:tag` spread tagged collection into variadic - `!go:pkg.Symbol` Go package-level var/const (e.g. `!go:os.Stdout`) - `!field:@service.Field` field access on service (e.g. `!field:@config.Host`) - `!field:!go:pkg.Symbol.Field` field access on Go symbol (e.g. `!field:!go:http.DefaultClient.Timeout`) - literal scalars (string/int/float/bool/null) Method constructors are configured via the `constructor.method` field (`method: "@factory.Create"`), not as a constructor argument. `$this.` in `type` and `func` fields, a tag's `element_type`, `!go:` arguments, and `!field:!go:` arguments resolves to the Go package path of the config file. The `method` field takes no `$this` — it addresses a service, not a package. Imports support `exclude`: ```yaml imports: - path: ./services/*.yaml exclude: - ./services/test_*.yaml ``` ## Service Defaults `services._default` sets the default `shared`, `public`, and `autoconfigure` flags for the services of the same file (per file; not inherited across imports). Any other field in `_default` is an error. Built-in defaults: `shared: true`, `public: false`, `autoconfigure: true`. ## Parameters Declared as plain scalar defaults (no `type` field; null and mapping values rejected): ```yaml parameters: port: 8080 timeout: 5s ``` The target type is contextual — taken from each constructor argument. One parameter may be injected as different types at different sites. Supported targets: `string`, `bool`, all int/uint widths, `float32`, `float64`, `time.Duration`, `time.Time`, and named types over those (static conversion is generated). `uintptr` unsupported. Runtime split: `Provider.Lookup(name) (any, error)` returns the raw value; `parameters.Caster` (`StandardCaster` by default, override via `WithParameterCaster`) converts it per injection site. Generated code calls both through the `parameters.Resolver` facade struct: `c.paramsResolver.Int("port")`. Unsupported targets and non-convertible defaults fail at generation time; runtime provider values are checked at construction with parameter name, service ID, argument index, raw and target types in the error. ## Spread Rules - Only one `!spread:` per constructor call - Must be the last argument - Inner expression must resolve to a slice - Target parameter must be variadic ## Pruning Public services (and public tags) are the generation roots. Services unreachable from a root are dropped after validation, together with the parameters only they injected. `--enable-pass=expose-all` makes everything public and disables pruning. ## Generated Container API ```go var DefaultContainerParameters *parameters.ProviderMap // injected YAML defaults; omitted when none survive pruning func NewContainer(params parameters.Provider, opts ...ContainerOption) *Container func (c *Container) GetServiceName() (ServiceType, error) func (c *Container) MustServiceName() ServiceType func WithContainerErrorHandler(handler func(serviceName string, err error)) ContainerOption func WithContainerParameterCaster(caster parameters.Caster) ContainerOption ``` `NewContainer(nil)` uses `DefaultContainerParameters`, or `parameters.ProviderNullInstance` when it is absent. Parameters no surviving service injects are pruned from the defaults after unreachable services are pruned; the variable is emitted only when at least one remains. Available providers: `NewProviderMap`, `NewProviderStructTag` (`di-param` struct tags), `NewProviderComposite` (last provider holding a name wins), `NewProviderNull`. ## Generated File Conventions - Generated files are `*_gen.go` - Banner: `// Code generated by gendi; DO NOT EDIT.` --- # Configuration Reference Source: https://gendi.dev/docs/configuration/ Complete reference for GenDI YAML configuration files. ## Schema Validation Add this line at the top of your YAML files for editor autocomplete and validation: ```yaml # yaml-language-server: $schema=https://raw.githubusercontent.com/gendi-org/gendi/master/gendi.schema.json ``` Supported editors: - **VS Code**: Install [YAML extension](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml) - **IntelliJ IDEA**: Built-in YAML support - **Vim/Neovim**: Use [yaml-language-server](https://github.com/redhat-developer/yaml-language-server) For local schema validation: ```yaml # yaml-language-server: $schema=./gendi.schema.json ``` ## Root Structure A config file has four optional top-level keys and nothing else: ```yaml imports: [] # other configs to load and merge before this file parameters: {} # scalar defaults, injected as %name% tags: {} # tag declarations services: {} # service definitions ``` Each of those keys has its own page: [Parameters](parameters.md), [Services](services.md), [Tags](tags.md) and [Imports](imports.md). How an individual argument is spelled is covered in [Arguments](arguments.md). ## See Also - [Design](../design.md) - [Custom Compiler Passes](../passes.md) - [Standard Library Services](https://github.com/gendi-org/gendi/blob/master/stdlib/README.md) - [Example App](https://github.com/gendi-org/gendi-example-app) --- # Services Source: https://gendi.dev/docs/configuration/services/ Services are objects constructed and managed by the container. ## Service Definition ```yaml services: service_id: # Optional: Explicit type (inferred from constructor if omitted) type: "github.com/myapp.Service" # Constructor configuration constructor: # Function constructor func: "github.com/myapp.NewService" # OR method constructor method: "@other_service.CreateService" # Constructor arguments args: - "@dependency" # Service reference - "%parameter%" # Parameter reference - "!tagged:tag.name" # Tagged services - "@.inner" # Inner service (decorators only) - "literal string" # Literal value - 123 # Literal number - true # Literal bool # Lifecycle (default: shared=true) shared: true # Singleton (cached) # shared: false creates new instance each time # Public API exposure public: true # Generate public getter method # Participation in autoconfigured tags (default: true) autoconfigure: true # Service aliasing alias: "other_service" # Alias to another service # Decoration decorates: "base_service" # Decorate another service decoration_priority: 10 # Higher priority decorators wrap first # Tagging tags: - "tag.name" # String shorthand, no attributes - name: "tag.name" # Every field except 'name' is an attribute attribute1: "value1" priority: 100 ``` A service ID must not end with `.inner` — that suffix is reserved for decorator expansion. ## The `type` Field `type` is optional. When omitted, the service type is the constructor's return type. When present it is a contract: the inferred constructor type must match it, or generation fails. It is not a conversion — declaring a supertype does not widen the service. --- # Constructors Source: https://gendi.dev/docs/configuration/constructors/ How a service's constructor may be declared: a plain function, a method of another service, or a generic function instantiated with explicit type arguments. How each argument is spelled is covered in [Arguments](arguments.md). ## Constructor Signatures A constructor — `func` or `method` — must return exactly one of: 1. `T` 2. `(T, error)` `T` is any type the container can check statically: concrete types, pointers, slices, maps, channels, and interfaces with methods. - A constructor returning `any`/`interface{}`, or a named type whose underlying type is an empty interface, is rejected at generation time: such a type makes every assignment valid and leaves nothing to verify - Generic constructors require explicit type arguments, so a bare type parameter never becomes a service type - Argument count and types are validated against the signature ## Method Constructors Use service methods to construct other services: ```yaml services: factory: constructor: func: "factory.New" shared: true processor: constructor: method: "@factory.CreateProcessor" args: - "@dependency" shared: false ``` Generated code: ```go func (c *Container) buildProcessor() (*Processor, error) { factory, err := c.getFactory() if err != nil { return nil, err } dep, err := c.getDependency() if err != nil { return nil, err } return factory.CreateProcessor(dep), nil } ``` ## Generic Constructors Support for Go generics with type arguments: ```yaml services: events: constructor: func: "github.com/gendi-org/gendi/stdlib.NewChan[github.com/myapp/events.Event]" args: - 100 # buffer size public: true ``` Generated code: ```go func (c *Container) buildEvents() (chan events.Event, error) { return make(chan events.Event, 100), nil } ``` `stdlib.NewChan` is a real generic function of the `stdlib` package; the generator recognizes it and emits the equivalent `make` expression through a dedicated inliner instead of calling it, so the generated file does not import `stdlib`. A generic constructor from any other package is instantiated and called normally: `pkg.NewPool[events.Event](100)`. --- # Parameters Source: https://gendi.dev/docs/configuration/parameters/ Parameters are scalar configuration values injected using `%name%` syntax. A declaration is just a default value — parameters have no declared type. The target type is contextual: it comes from the constructor argument the parameter is injected into, so the same parameter can be requested as different types at different injection sites. Parameters are immutable: a container reads them through its provider and never writes them back, so the values a container was built with hold for its whole lifetime. ## Parameter Definition ```yaml parameters: app_name: "MyApp" port: 8080 timeout: 30s debug: true rate_limit: 99.5 ``` Each default must be a plain scalar; null and mapping values are rejected. ## Supported Target Types The constructor argument a parameter is injected into must have one of these types (or a named type whose underlying type is one of them — a static conversion is generated): | Target | Conversion method | Notes | |--------|-------------------|-------| | `string` | `ToString` | numeric values format canonically | | `bool` | `ToBool` | strings parse via `strconv.ParseBool` | | `int`, `int8`–`int64` | `ToInt`–`ToInt64` | range-checked; strings parse base 10 | | `uint`, `uint8`–`uint64` | `ToUint`–`ToUint64` | sign- and range-checked | | `float32`, `float64` | `ToFloat32`, `ToFloat64` | integers must convert exactly | | `time.Duration` | `ToDuration` | strings via `time.ParseDuration`, integers as nanoseconds | | `time.Time` | `ToTime` | strings parse as RFC3339 | `uintptr` and complex types are not supported. A parameter injected into any other target type fails generation. Exact `time.Duration` targets use `ToDuration`; a named type defined as `type Timeout time.Duration` has underlying `int64` and therefore uses `ToInt64` — a string default like `"5s"` for such a target fails at generation time. Use exact `time.Duration` or a numeric default instead. ## Supplying Values at Runtime The YAML declarations are defaults. The generator emits them as `var DefaultParameters = parameters.NewProviderMap(...)`, and `NewContainer(nil)` uses that provider. Pass a different `parameters.Provider` to take values from somewhere else: ```go container := di.NewContainer(parameters.NewProviderComposite( di.DefaultContainerParameters, // YAML defaults parameters.NewProviderStructTag(appConfig), // overrides from the app config )) ``` Ready-made providers: | Provider | Source of values | |----------|------------------| | `parameters.NewProviderMap(map[string]any)` | in-memory map | | `parameters.NewProviderStructTag(v)` | struct fields tagged `di-param` | | `parameters.NewProviderComposite(p...)` | several providers, later wins | | `parameters.NewProviderNull()` / `ProviderNullInstance` | nothing; every lookup misses | Only the parameters an injected argument actually references reach the generated map: after unreachable services are pruned, every parameter no surviving service injects is dropped from the defaults. A parameter declared in an imported file but never injected — a common outcome of importing `stdlib/gendi.yaml` for a single service — therefore has no entry in `DefaultParameters`. When nothing survives, the variable is not emitted at all and `NewContainer(nil)` falls back to `parameters.ProviderNullInstance`, which reports every name as missing. Pruning does not skip validation: declared defaults are checked against their injection sites before pruning runs (see [Generation-Time Validation](#generation-time-validation)). `ProviderComposite` queries its providers from last to first, skipping the ones that report `parameters.ErrParameterNotFound`, so the last provider that has a name wins. A name no provider knows fails the getter with an error wrapping `parameters.ErrParameterNotFound`. `ProviderStructTag` walks a struct (pointers are dereferenced): ```go type Config struct { HTTP HTTPConfig // untagged: searched through Port int `di-param:"port"` Features map[string]string `di-param:"feature"` // feature.beta Secret string `di-param:"-"` // never exposed } type HTTPConfig struct { Timeout time.Duration `di-param:"http.timeout"` } ``` - A tagged scalar field answers exactly its tag name - An untagged struct field is traversed transparently — nested fields keep the names their own tags give them - A tagged struct field acts as a prefix (`tag.field`); asked for the tag itself it returns the struct as a value, which suits `time.Time` and fails the cast for anything else - A tagged map with string keys answers `tag.key`, nested maps `tag.key.subkey` - Unexported fields and `di-param:"-"` are skipped - Values are read reflectively, so named types are normalized to their base kind before casting ## Lookup and Casting At runtime a parameter is resolved in two steps: the provider locates the raw value (`Provider.Lookup(name)`), then the container's caster converts it to the injection site's target type. Generated code performs both steps through a single `parameters.Resolver` facade call (`c.paramsResolver.Int("port")`); the facade is a concrete struct over `Provider` + `Caster`, so the two responsibilities stay independently replaceable. The default `parameters.StandardCaster` rejects lossy conversions: float→integer, bool→anything, values that overflow the target, inexact integer↔float conversions, NaN and infinities, and named input types. Cast errors name the raw value, its type, and the target type; the generated wrapping adds the parameter name, service ID, and argument index. A custom caster can override individual conversions: ```go type LenientCaster struct { parameters.StandardCaster } func (LenientCaster) ToInt(value any) (int, error) { // custom policy } container := di.NewContainer(nil, di.WithContainerParameterCaster(LenientCaster{})) ``` Custom casters can build rejection errors with `parameters.NewCastError(value, "int")` to keep their messages consistent with the standard policy. Every rejection wraps the `parameters.ErrCannotCast` sentinel, so applications can distinguish cast failures from missing parameters (`parameters.ErrParameterNotFound`) via `errors.Is`. ## Generation-Time Validation Declared defaults are validated during generation by executing the real `StandardCaster` against every usage's target type, so a default like `timeout: "5x"` injected as `time.Duration` fails generation, not startup. Values supplied by a runtime provider are checked at construction time. Validation runs before pruning and covers every injection site, including those of services that are later pruned as unreachable. A parameter with no injection site at all has no target type to validate against; it is neither checked nor emitted. ## Parameter References Reference parameters in constructor arguments using `%param_name%`: ```yaml services: server: constructor: func: "server.New" args: - "%app_name%" - "%port%" ``` ## Parameter Overrides Later imports override earlier parameter values: ```yaml # base.yaml parameters: db_dsn: "postgres://localhost/dev" # production.yaml imports: - ./base.yaml parameters: db_dsn: "postgres://prod-host/app" # Overrides base.yaml ``` --- # Arguments Source: https://gendi.dev/docs/configuration/arguments/ ## Special Tokens ### The `$this` Token `$this` is replaced with the Go package path of the config file's directory, found by walking up to the nearest `go.mod` and computing the path relative to the module root. Every file resolves its own `$this`, so an imported config anchors at its own location, not at the importing file's. When package resolution fails, `$this` is left as-is and surfaces as a "symbol not found" generation error. **Use in `type` field** — anywhere in the type expression: ```yaml services: logger: type: "*$this.Logger" # → *github.com/myapp/log.Logger # also []$this.Handler, map[string]$this.Route, chan $this.Event ``` **Use in `func` field** — at the start of the path, and inside generic type arguments: ```yaml services: logger: constructor: func: "$this.NewLogger" # → github.com/myapp/log.NewLogger pool: constructor: func: "$this.NewPool[$this.Message]" ``` **Use in `method` field:** ```yaml services: child: constructor: method: "@factory.CreateChild" # → factory.CreateChild (no $this needed) ``` **Use in `!go:` arguments:** ```yaml services: logger: constructor: func: "$this.NewLogger" args: - "!go:$this.DefaultLevel" # → !go:github.com/myapp/log.DefaultLevel ``` **Use in `!field:!go:` arguments:** ```yaml services: server: constructor: func: "$this.NewServer" args: - "!field:!go:$this.DefaultConfig.Host" # → !field:!go:github.com/myapp.DefaultConfig.Host ``` **Benefits:** - Eliminates repetitive package paths - Makes configuration more portable - Clearer intent (local vs external types) ### Service References Reference other services using `@service_id`: ```yaml services: database: constructor: func: "db.New" repository: constructor: func: "repo.New" args: - "@database" # Injects database service ``` ### Parameter References Reference parameters using `%param_name%`: ```yaml parameters: db_dsn: "postgres://localhost/app" services: database: constructor: func: "db.New" args: - "%db_dsn%" # Injects parameter value ``` ### Tagged References Reference tagged service collections using `!tagged:tag_name`: ```yaml services: server: constructor: func: "server.New" args: - "!tagged:handler" # Injects []Handler ``` ### Inner Service Reference Reference the decorated service using `@.inner` (decorators only): ```yaml services: decorator: constructor: func: "decorator.New" args: - "@.inner" # The service being decorated decorates: base_service ``` `@.inner` must be passed directly as an argument. Composed forms such as `!spread:@.inner` are invalid. ### Spread Operator Unpack slices into variadic parameters using `!spread:`: ```yaml services: server: constructor: func: "server.New" # func New(handlers ...Handler) args: - "!spread:!tagged:handler" # Unpacks []Handler into ...Handler ``` **Spread with service reference:** ```yaml services: handlers: constructor: func: "app.GetHandlers" # Returns []Handler server: constructor: func: "server.New" # func New(handlers ...Handler) args: - "!spread:@handlers" # Unpacks handlers into ...Handler ``` **Spread Rules:** - Only one `!spread:` per constructor call - Must be the last argument - Inner expression must resolve to a slice - Target parameter must be variadic ## Argument Syntax Constructor arguments support multiple syntaxes: | Syntax | Type | Example | |--------|------|---------| | `@service.id` | Service reference | `@database` | | `@.inner` | Inner service | `@.inner` (decorators only) | | `%param.name%` | Parameter | `%db_dsn%` | | `!tagged:tag` | Tagged collection | `!tagged:handler` | | `!spread:@service` | Spread service slice | `!spread:@handlers` | | `!spread:!tagged:tag` | Spread tagged slice | `!spread:!tagged:middleware` | | `!go:pkg.Symbol` | Go package-level var/const | `!go:os.Stdout` | | `!field:@service.Field` | Service field access | `!field:@config.Host` | | `!field:!go:pkg.Symbol.Field` | Go symbol field access | `!field:!go:http.DefaultClient.Timeout` | | `"string"` | String literal | `"localhost"` | | `123` | Integer literal | `8080` | | `45.6` | Float literal | `3.14` | | `true`/`false` | Boolean literal | `true` | | `null` | Null literal | `null` | > **Note:** `@service.Method` is not a constructor argument — a method > constructor is configured via the `constructor.method` field > (`method: "@factory.Create"`). Used as an argument, `@factory.Create` would > be parsed as a plain reference to a service whose ID is `factory.Create`. ### Type Compatibility GenDI validates argument types at generation time: - Service references must match parameter types - Parameters are converted to target types - Tagged collections must match slice element types - Literals are type-checked against parameters ### Variadic Arguments Variadic constructors accept multiple arguments: ```yaml services: logger: constructor: func: "log.New" named_logger: constructor: method: "@logger.With" # func With(args ...any) Logger args: - "channel" # First variadic arg - "database" # Second variadic arg ``` Generated code: ```go return logger.With("channel", "database"), nil ``` --- # Service Lifecycle Source: https://gendi.dev/docs/configuration/lifecycle/ Every service is either shared — built once and cached on the container — or non-shared, built anew on every access. `shared: true` is the default. ## Shared Services (Singletons) ```yaml services: database: constructor: func: "db.New" shared: true # Default ``` - Created once on first access - Same instance returned on subsequent calls - Construction is serialized by the container mutex (see [Locking](#locking)) - Suitable for: databases, HTTP clients, loggers ## Non-Shared Services (Factories) ```yaml services: request_context: constructor: func: "ctx.New" shared: false ``` - New instance created on each access - No caching; the instance is never stored on the container - Suitable for: request handlers, temporary objects ## When the Cache Disappears A shared service that is neither public nor an alias, and that exactly one service depends on, is generated without a cache field: with a single holder anchored by a shared ancestor, caching is unobservable, so the generator drops it. The guarantee that survives is the observable one — that holder still sees one instance for the lifetime of the container. This is why reading the generated file can suggest that `shared: true` was ignored. It was not; there was simply nothing for the cache to protect. ## Locking The container has exactly one mutex, and only the public getters take it — for both lifecycles. It guards the shared-instance fields of the whole graph, not just the service being fetched, so one public getter call builds its entire subgraph under a single lock. The internal getters services use to reach each other are lock-free, including those of shared services: they run only inside a public getter that already holds the mutex. The mutex is a plain `sync.Mutex` and is not reentrant, so a constructor must not call a public getter of its own container — that deadlocks. Take dependencies as constructor arguments instead. --- # Aliases and Visibility Source: https://gendi.dev/docs/configuration/visibility/ Which services get a getter, what name they answer to, and why a service you configured may not appear in the generated container at all. ## Service Aliases Create multiple names for the same service: ```yaml services: logger.impl: constructor: func: "log.New" # String shorthand: the whole service definition is "@target" logger: "@logger.impl" # Expanded form, needed when the alias also sets public or type log: alias: "logger.impl" # the @ prefix is optional here public: true ``` Aliases always inherit the target service lifecycle. Do not set `shared` on an alias; explicit `shared: true` and `shared: false` are both configuration errors. ## Public Services Generate public getter methods for services: ```yaml services: user_repo: constructor: func: "repo.NewUserRepository" public: true ``` Generated methods: ```go func (c *Container) GetUserRepo() (*UserRepository, error) func (c *Container) MustUserRepo() *UserRepository ``` ## Reachability Public services are the roots of the generated container: only they and the services reachable from them through constructor arguments are emitted. Everything else is pruned after validation and never reaches the generated file. - Public tags count as roots too — a `public: true` tag keeps every service it collects - A config in which nothing is public generates a container with no services - Pruning happens after all type checking, so an unreachable service with a broken constructor still fails generation - Importing a large config costs nothing at runtime: the services you do not reach are not generated This is why an imported service can be missing a getter you expected: mark it `public: true`, reach it from something public, or generate with `--enable-pass=expose-all`, which makes every service public and thereby disables pruning. --- # Decorators Source: https://gendi.dev/docs/configuration/decorators/ Decorators wrap existing services to add behavior: ```yaml services: logger: constructor: func: "log.New" logging_decorator: constructor: func: "log.NewDecorator" args: - "@.inner" # Receives the decorated service decorates: logger decoration_priority: 10 metrics_decorator: constructor: func: "metrics.NewDecorator" args: - "@.inner" decorates: logger decoration_priority: 20 # Higher priority wraps first ``` **Execution order:** `metrics(logging(logger))` ## Rules - Must use `@.inner` in constructor args to receive the decorated service - Multiple decorators on same service are ordered by `decoration_priority` (descending) - The decorated service's own ID becomes an alias to the outermost decorator, so `@logger` resolves to `metrics(logging(logger))` - The original definition is moved to `.inner`, which `@.inner` points at (the `.inner` suffix is reserved and cannot be used in service IDs) - The decorator type must be compatible with the decorated service type - The base service and each decorator keep their own `shared` setting: a shared service may be wrapped by a non-shared decorator and vice versa - The decorator does not inherit tags of the decorated base service. Tags declared on the base stay on its inner, undecorated definition, so tagged collections receive the undecorated instance (Symfony semantics) — tag the decorator explicitly if the collection should get the decorated one - A decorator cannot itself be decorated --- # Tags Source: https://gendi.dev/docs/configuration/tags/ Tags enable collecting multiple services that implement a common interface. Tagged collections are desugared into a `stdlib.MakeSlice` constructor during analysis, so a config that declares any tag makes the generator load `github.com/gendi-org/gendi/stdlib` — the module must be resolvable from the module being generated into (installing GenDI as a tool dependency covers this). This holds even for a tag only a compiler pass consumes. The `MakeSlice` call is inlined into a slice literal, so the generated file itself does not import `stdlib`. ## Tag Definition ```yaml tags: handler: # Optional: Element type inferred from usage if omitted element_type: "github.com/myapp.Handler" # Optional: Sort by tag attribute sort_by: "priority" # Optional: Generate public tag getter public: true # Optional: Auto-tag services implementing the interface autoconfigure: true ``` ## Tag Attributes - **`element_type`**: Go type of tagged services (required when `public: true` or `autoconfigure: true`) - **`sort_by`**: Attribute name for sorting (incompatible with `autoconfigure`) - **`public`**: Generate public getter for tagged collection - **`autoconfigure`**: Automatically tag every service whose type is assignable to `element_type` ## Declared and Implicit Tags A tag does not have to be declared: referencing a name from a service's `tags` list or from a `!tagged:` argument creates it implicitly. Declaring a tag is what buys the optional behaviour — only a declared tag can set `element_type`, `sort_by`, `public`, or `autoconfigure`. When `element_type` is omitted, it is inferred from the constructor arguments that consume the tag via `!tagged:name`; if several constructors consume the same tag, their element types must be compatible. A tag with neither a declared nor an inferable element type — nothing consumes it — is silently skipped: no collection is built and the services carrying it keep their tag without effect. `public` and `autoconfigure` tags always need an explicit `element_type`, since a public getter has to have a type and autoconfigure matches against one. Once the element type is known, every tagged service's type must be assignable to it, or generation fails naming the service and both types. ## Tagged Services Each tag entry is either a string (shorthand for `{name: "..."}`) or a mapping with `name` and optional attributes: ```yaml services: handler1: constructor: func: "github.com/myapp.NewHandler1" tags: - name: "handler" priority: 100 handler2: constructor: func: "github.com/myapp.NewHandler2" tags: - name: "handler" priority: 200 handler3: constructor: func: "github.com/myapp.NewHandler3" tags: - "handler" # string shorthand — no attributes server: constructor: func: "github.com/myapp.NewServer" args: - "!tagged:handler" # Receives []Handler ordered by service ID # (the handler tag declares no sort_by) ``` ## Tag Sorting When `sort_by` is specified, services are sorted by the tag attribute: ```yaml tags: middleware: element_type: "github.com/myapp.Middleware" sort_by: "order" # Sort by "order" attribute services: auth: tags: - name: "middleware" order: 10 logging: tags: - name: "middleware" order: 1 metrics: tags: - name: "middleware" order: 5 ``` Result: `[auth, metrics, logging]` — descending by the attribute, higher value first. The attribute is read as an integer (a numeric scalar or a decimal string); a service whose tag omits it sorts as `0`. Services with equal values are ordered by service ID. Without `sort_by`, the collection is ordered by service ID. ## Public Tag Getters ```yaml tags: handler: element_type: "github.com/myapp.Handler" public: true ``` Generated method: ```go func (c *Container) GetTaggedWithHandler() ([]Handler, error) ``` ## Auto-Configuration ```yaml tags: handler: element_type: "github.com/myapp.Handler" autoconfigure: true ``` Every service whose resolved type implements `Handler` — that is, is assignable to it — is automatically tagged with `handler`. The constructor does not have to return the interface itself; a concrete return type that satisfies it is enough. **Rules:** - Only a declared tag can autoconfigure; `element_type` is required and must be an interface type - Cannot be combined with `sort_by` - Services are tagged at IR build time, after decorator expansion, so a decorated service participates through its outermost decorator's type - Aliases, decorator `.inner` services, and services with `autoconfigure: false` (set directly or through `_default`) are excluded - Explicit tags are kept; autoconfigure only adds the services that are missing, and the resulting collection is ordered by service ID - An autoconfigured collection may legitimately be empty With the tag above and a decorated handler, only the decorator is autoconfigured: ```yaml services: base: constructor: func: "github.com/myapp.NewBaseHandler" decorated: constructor: func: "github.com/myapp.NewDecoratedHandler" args: - "@.inner" decorates: "base" ``` `decorated` is tagged if its type implements `Handler`; `base` — now an alias to `decorated` — and the generated `decorated.inner` never participate. --- # Service Defaults Source: https://gendi.dev/docs/configuration/defaults/ The reserved `_default` entry sets the default `shared`, `public`, and `autoconfigure` flags for the services declared in the same file: ```yaml services: _default: shared: true public: false autoconfigure: false logger: constructor: func: "$this.NewLogger" public: true # explicit value wins over the default ``` - Applies per file — an imported config keeps its own defaults, and inherits none from the importing file - Only `shared`, `public`, and `autoconfigure` are allowed; `type`, `constructor`, `alias`, `decorates`, `decoration_priority`, and `tags` in `_default` are configuration errors - Without `_default`, the built-in defaults are `shared: true`, `public: false`, `autoconfigure: true` - `_default` is not a service and gets no getter --- # Imports Source: https://gendi.dev/docs/configuration/imports/ Configuration files can import and override other configurations. ## Import Syntax An entry is either a string or a mapping with `path` (and optionally `exclude`); the two forms can be mixed in one list: ```yaml imports: - ./base.yaml # Relative path - ./services/*.yaml # Glob pattern - ./**/gendi.yaml # Recursive glob - github.com/pkg/stdlib/gendi.yaml # Module import (file or glob, never a bare module path) - path: ./config/*.yaml # Mapping form exclude: [./config/dev_*.yaml] ``` ## Import Resolution - Entries are processed in declaration order, and a glob's matches are expanded in lexicographic order — both matter, because later definitions win - Importing recursively is fine; an import cycle is a generation-time error - An import is classified by its form: a multi-segment path whose first segment contains a dot (`example.com/...`) names a Go module; everything else — including single-segment names like `base.yaml` — is a local path. For a local directory whose name contains a dot, use the `./` spelling (`./assets.d/*.yaml`). A bare module-shaped spelling always selects the module when it exists, regardless of a same-spelled local path; use `./` to select the local path explicitly - Absolute filesystem paths are not allowed - Relative paths resolved from importing file's directory - Glob patterns expanded using doublestar matching; a glob over an existing directory that matches nothing is a silent no-op, but a glob whose base directory does not exist is a generation-time error - Every config, including the root, is confined immediately before loading. Imported candidates use the module of the importing file (or the named module) as their boundary: after exclusions are applied, each candidate is resolved through symlinks and checked against the boundary — a file whose real path is outside is a generation-time error (exclude unwanted symlinked matches to keep a broad glob loadable). A candidate whose real path belongs to a nested Go module is also rejected unless that module was selected through a module-path import. Every import occurrence is loaded independently and keeps its addressed path, so a config imported through a symlink anchors its own relative imports and `$this` at the symlink's directory. Cycle detection identifies only active imports by real path - Imports are merged depth-first in declaration order: each imported file's imports are merged before that file, and the importing file is merged after all of its imports - Later definitions override earlier ones - Services with same ID are replaced completely - Every occurrence in the import graph participates in the merge. With diamond imports where A imports B and then C, and both import D, the merge order is D, B, D, C, A. The second occurrence of D therefore re-introduces definitions that B overrode. Put final overrides in A, or in a file imported after every branch that can re-introduce the original definition ## Import Exclusions Exclude specific files from glob pattern imports: ```yaml imports: # Load all services except test files and internal files - path: ./services/*.yaml exclude: - ./services/test_*.yaml - ./services/internal/*.yaml # Glob in subdirectories with exclusions - path: ./config/**/*.yaml exclude: - ./config/**/dev_*.yaml ``` **Exclusion Features:** - Exclusions are masks over the files the import found — they never touch the filesystem themselves - Supports full glob syntax (`*`, `?`, `[]`, `**`) - Addressed like the import and must use the same form: a local import takes local masks, a module import takes masks inside the same module (`example.com/mod/services/skip.yaml`) - A mask matching a directory on a file's path excludes the whole subtree - A mask that matches nothing is a silent no-op; only a malformed pattern is an error - Applied before the sandbox check, so unwanted symlinked matches can be excluded explicitly ## Import Merging When merging configurations: 1. **Parameters**: Later values override earlier ones 2. **Tags**: Later definitions override earlier ones 3. **Services**: Later services completely replace earlier ones with same ID ## Module Imports Import from Go modules: ```yaml imports: - github.com/gendi-org/gendi/stdlib/gendi.yaml ``` The module is located through the `go.mod` graph (including `replace` directives) and the named file is loaded from it. A module import must name a file or glob explicitly — a bare module path is an error. Publishing such a file from a library of your own has conventions of its own — see [Shipping Wiring in a Library](../library-wiring.md). Module lookup uses the module containing the importing config. When the root config is outside every Go module, the CLI uses the module containing the generated output as the lookup context while keeping the root config confined to its own directory. Library callers provide these independently as `yaml.LoadConfig(path, boundary, moduleContext)`. Resolution never depends on the process working directory. ## Best Practices **Recommended structure:** ``` project/ ├── gendi.yaml # Root (imports only) ├── services/ │ ├── database.yaml │ ├── http.yaml │ └── logging.yaml └── config/ ├── dev.yaml └── prod.yaml ``` **Root file:** ```yaml imports: - ./services/*.yaml - ./config/dev.yaml ``` **Benefits:** - Small, focused configuration files - Easy to navigate and maintain - Clear separation of concerns - Environment-specific overrides --- # Complete Example Source: https://gendi.dev/docs/configuration/example/ ```yaml # Import stdlib and base services imports: - github.com/gendi-org/gendi/stdlib/gendi.yaml - ./services/base.yaml # Configuration parameters parameters: app_name: "MyApp" db_dsn: "postgres://localhost/myapp" http_timeout: "30s" # Tag definitions tags: handler: element_type: "github.com/myapp.Handler" sort_by: "priority" public: true # Service definitions services: database: constructor: func: "github.com/myapp/db.New" args: - "%db_dsn%" shared: true user_repo: constructor: func: "github.com/myapp/repo.NewUserRepository" args: - "@database" shared: true public: true home_handler: constructor: func: "github.com/myapp/handlers.NewHome" args: - "@user_repo" tags: - name: "handler" priority: 10 api_handler: constructor: func: "github.com/myapp/handlers.NewAPI" args: - "@user_repo" tags: - name: "handler" priority: 20 http_server: constructor: func: "github.com/myapp/server.New" args: - "%app_name%" - "!spread:!tagged:handler" public: true # Decorator for logging logging_middleware: constructor: func: "github.com/myapp/middleware.NewLogging" args: - "@.inner" - "@stdlib.logger" decorates: http_server decoration_priority: 10 ```