Schemas
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
codegen.GenerateModel: Emits application-owned concrete Go structs (Config, etc.).codegen.GenerateBinder: Emits typed resolution and binder helpers. In this initial phase, this legacy binder still relies on library-assisted runtime functions (gogenconf.BindingValue,gogenconf.ResolveField, etc.) and requiresgithub.com/arran4/gogenconfat runtime until #6 is complete.
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:
config_document_generated.go: Document, Section, Entry, Comment, Blank, Version, and document mutation methods.config_expr_generated.go: Expr, Literal, Call, and CloneExpr.config_parse_generated.go: native document parser and expression parser with v0/v1 comment ownership and version checking.config_format_generated.go: deterministic document and expression formatter.
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)
- #5: Generate an application-owned native config runtime instead of importing gogenconf; the project CLI dogfoods it for syntax tooling.
- #6: Generate static resolver and declarer code for dependency-free binders.
- #7: Generate application-owned provider support and emit only schema-required runtime features.
- #8: Add a first-class
gogenconf generatecommand and canonical project generation contract. - #9: Generate a schema-specialized application config API.
- #10: Isolated consumer gate proving no gogenconf runtime dependency.
Defaults, examples, comments, and evolution
Defaultis an unresolved expression inserted bySchema.Seed.Examplesare documentation only.Schema.Sampleillustrates optional fields and repeated sections as inactive comments; it never activates examples.Schema.Enrichadds missing fields/defaults and sections, refreshes managed documentation, and preserves explicit values, user comments, and unknown content. A changed default does not overwrite an existing override. Enrichment is idempotent; the evolution goldens make this visible.#is user-owned and##is managed documentation in versioned v1+ files. All unversioned/v0 comments are user-owned, even##or###headings. On upgrade,## Headingbecomes# # Headingso a reload cannot claim it as managed.- Registered consecutive
Migrationsoperate transactionally on unresolved documents. They advance versions only on success; missing paths and unsupported future versions fail. Applications own semantic renames and compatibility transformations. Parsing and loading never implicitly save a file. InputOnlyfields remain available throughSchema.Fieldfor compatibility, migration, inspection, and deliberate removal, butWritableFieldexcludes them from normal editors. They generate no runtime member.RuntimeOnlydoes the converse: a concrete application member with no persisted input.Required,Sensitive, repeated sections, and eager/provider policy are explicit schema metadata, not inferred from the Go type.
For example, a credential field can have no Default but have an Example
from_file(from_env(CREDENTIAL_FILE)). Seed() leaves the field absent;
Sample() emits an inactive ## credential from_file(...) illustration.
Enrich() does not activate that example. Conversely, Default: Literal{"3"}
for retries inserts an active value when missing. Explicit values always win
over a changed schema default. Managed documentation is associated with fields
and section blocks; user comments never become schema property merely because
their prose resembles documentation.
Schema declares Version and ordered Sections. SectionDefinition gives Name,
GoName/GoType, Documentation, Fields, Repeated and ExampleNames. Field carries
Key, GoName/GoType, Default, Examples, Documentation, Required, Sensitive,
InputOnly, RuntimeOnly, Deprecated, Policy and legacy Environment fallback metadata.
Deprecated suppresses new sample/enrichment entries; InputOnly additionally blocks
normal editing/runtime targets. Required checks missing declarations, not arbitrary
application-specific value validation. Sensitive is inspection/error policy, not
encryption at rest. Applications must validate their domain constraints after binding.