Code Generation

Application schema and generated Go

Applications define content in Go; the native language does not know their fields:

schema := gogenconf.Schema{
    Version: 1,
    Sections: []gogenconf.SectionDefinition{{
        Name: "service", GoName: "Service", GoType: "ServiceConfig",
        Fields: []gogenconf.Field{
            {Key: "endpoint", GoName: "Endpoint", GoType: "string",
                Default: gogenconf.Literal{Value: "https://example.test"},
                Documentation: []string{"Service endpoint."}},
            {Key: "credential", GoName: "Credential", GoType: "[]byte",
                Sensitive: true, Required: true,
                Examples: []gogenconf.Expr{gogenconf.Call{
                    Name: "from_env", Args: []gogenconf.Expr{
                        gogenconf.Literal{Value: "CREDENTIAL"},
                    },
                }}},
        },
    }},
}

An application-local go:generate driver writes the outputs of:

model, err := codegen.GenerateModel(schema, codegen.Options{Package: "appconfig"})
// Check err and write model to the application's model package.
binder, err := codegen.GenerateBinder(schema, codegen.Options{
    Package: "configbinding", ConfigImport: "example.org/myapp/appconfig",
})
// Check err and write binder to the application's binding package.

ConfigImport names the application model package. The optional LibraryImport defaults to github.com/arran4/gogenconf. Generated files carry the canonical Code generated by gogenconf. DO NOT EDIT. header. The CLI is language tooling; generation uses an application-local driver, not runtime schema/plugin loading.

For example, put //go:generate go run ./internal/generate in your application package. That driver imports your Go schema and gogenconf/codegen, checks errors, and writes model/binder files. The runnable driver demonstrates this without installing a generator binary. Arbitrary Go schema values cannot safely be dynamically discovered by a generic CLI.

The generated model is concrete application-owned Go, not aliases or AST fields:

type Config struct { Service ServiceConfig }
type ServiceConfig struct {
    Endpoint   string
    Credential []byte
}

The generated binder’s Resolve(ctx, doc, registry, lookup) returns that Config using explicit typed calls, with no reflection-based struct population. The optional lookup supplies legacy environment fallbacks described by the schema. Authored values take precedence over environment fallbacks, then schema defaults; transient defaults inserted by enrichment retain that distinction until persisted. Supported static mappings are deliberately narrow: string, []byte, []string (the application registers its list semantics), and runtime-only bool fields. Invalid/ambiguous mappings fail generation or compilation.

config file → Document / Expr → Schema + migrations + enrichment
                                      ↓
                               generated binder
                                      ↓
                          application-owned Go Config

This flow is one-way. Edit and persist the original Document; resolved values have lost provenance and cannot be converted back through a Config-to-Document API.

Generation architecture and dependency-free runtime

gogenconf is transitioning to a build-time-only generator where generated application code has zero runtime dependencies on gogenconf (issue #4).

Current generation boundary

New capability: application-owned native runtime (#5)

Applications can now generate a native configuration runtime directly into application-owned code using codegen.GenerateRuntime or codegen.PlanRuntime:

files, err := codegen.GenerateRuntime(codegen.RuntimeOptions{
    Package: "myconfig",
})
if err != nil {
    return err
}
if err := files.WriteToDir("internal/myconfig"); err != nil {
    return err
}

This emits:

The generated code depends solely on the Go standard library (bufio, fmt, io, strconv, strings, unicode). It contains no imports of, aliases to, or runtime forwarding back to github.com/arran4/gogenconf.

Production dogfooding

The shipped gogenconf syntax CLI now generates and checks in internal/nativeconfig through the repository-local internal/runtimegen driver. format, validate, expr format, and expr validate use that generated runtime in production. The root gogenconf parser and formatter remain the canonical public implementation and the differential-conformance oracle; they are not a normal fallback for the CLI syntax path. This demonstrates the dependency-free native parsing boundary from #5, while complete generated-binder dependency removal remains #6 and generator/product parity follow-up remains #12/#19.

Roadmap to build-time-only generation (#4)