Documentation
¶
Overview ¶
Package genapp renders templates into formatted Go files.
A code generator holds a set of templates and, for each file it produces, executes one of them and formats the result. GoGenApp is that loop:
templates, err := repo.New(
repo.FromFS(assets, ""),
repo.WithFuncMap(golang.FuncMap(mangling.MakeGoMangler())),
)
if err != nil {
return err
}
app, err := genapp.New(
genapp.WithTemplates(templates),
genapp.WithOutputPath("./generated"),
genapp.WithFormatOptions(
formatting.WithImportGroups("github.com/go-openapi", baseImport),
),
)
if err != nil {
return err
}
if err := app.RenderFile("models/pet.go", "modelValidator", pet); err != nil {
return err
}
GoGenApp.Render writes to an io.Writer and GoGenApp.RenderFile writes a file under the output path, creating the directories it needs.
Where the templates come from, what funcmap they run with and which of them a run reaches are settled by github.com/go-openapi/codegen/templates-repo, and this package re-exports none of it: build the repository, then hand it over with WithTemplates.
Formatting ¶
Rendered Go goes through github.com/go-openapi/codegen/formatting, which prunes the imports nothing uses, groups the rest and prints in gofmt style. It never resolves a missing import and never runs the go command, so a template that forgets an import produces a file that does not compile rather than one that differs from machine to machine.
GoGenApp.RenderFile formats a target ending in ".go" and copies anything else through. Pass WithSkipFormatFunc to decide differently, or WithSkipFormat to write every target unformatted, which is worth doing when a template is misbehaving and the parse error hides the output.
Writing outside the output path ¶
A generator names its targets after the models and operations of a spec, so the spec decides what GoGenApp.RenderFile writes. RenderFile therefore refuses an absolute target, and one that climbs out of the output path with "..", whether or not the caller asks to be confined.
Writes go through os.Root, which checks each symbolic link as it walks the path. RenderFile removes a link standing at the target instead of following it, so the file it pointed at keeps its content, and refuses a link on the way to the target. Renaming replaces a name rather than the file behind it, so another hard link to the target keeps its own content. A directory, a device, a socket or a named pipe at the target is refused rather than overwritten.
WithRoot widens the boundary from the output path to a directory above it, for a generator writing into several directories of one tree:
app, err := genapp.New(
genapp.WithTemplates(templates),
genapp.WithOutputPath("./gen/models"),
genapp.WithRoot("./gen"),
)
Two things sit outside this. GoGenApp.TidyModule runs the go command, which writes go.mod and go.sum itself, and no root reaches into another process. Reads run unconfined too: GoGenApp.PackagePath and GoGenApp.EnclosingModule walk up from the output path looking for a go.mod, and that go.mod usually sits above any root worth setting.
os.Root confines path resolution and no more. It does not stop traversal of a bind mount, a /proc special file or a device file.
Where the code lands ¶
A generator has to write the imports that reach the code it produces, and that means knowing the import path of the tree it is writing into. GoGenApp.PackagePath answers it by reading the go.mod above the output path:
module example.com/petstore declared in /src/petstore/go.mod output path /src/petstore/gen/models PackagePath example.com/petstore/gen/models
GoGenApp.ModuleRequired answers the other half: whether the output path sits outside every module, and so needs a go.mod of its own before anything there can be built.
Modules ¶
GoGenApp.InitModule writes a go.mod for the generated tree, as "go mod init" would, without running it:
err := app.InitModule(
genapp.WithModulePath("example.com/petstore/gen"),
genapp.WithRequire("github.com/go-openapi/strfmt", "v0.24.0", false),
)
GoGenApp.TidyModule runs "go mod tidy", and is the one thing here that needs a Go toolchain:
err := app.TidyModule(ctx, genapp.WithTidyGoVersion("1.25.0"))
Everything else runs the go command never and reads the environment never, so a generated tree can be laid down and formatted on a machine with no Go installed. Resolving the versions a module ends up with is the exception, because it means walking the module graph and the checksum database, and reproducing that would mean reproducing the go command.
Concurrency ¶
A GoGenApp holds no state between calls, so GoGenApp.Render and GoGenApp.RenderFile may run concurrently. Each render borrows its buffer from github.com/go-openapi/swag/pools/shared and gives it back before returning, so a generator writing a few hundred files recycles a handful of buffers rather than allocating one per file.
Index ¶
- type Error
- type GoGenApp
- func (g *GoGenApp) EnclosingModule() (modulePath, moduleDir string, err error)
- func (g *GoGenApp) InitModule(opts ...ModOption) error
- func (g *GoGenApp) ModuleRequired() (bool, error)
- func (g *GoGenApp) PackagePath() (string, error)
- func (g *GoGenApp) Render(w io.Writer, name string, data any) error
- func (g *GoGenApp) RenderFile(target, name string, data any) error
- func (g *GoGenApp) Templates() *repo.Repository
- func (g *GoGenApp) TidyModule(ctx context.Context, opts ...TidyOption) error
- type ModOption
- type Option
- func WithFormatOptions(opts ...formatting.Option) Option
- func WithImportsReporter(report func(template string, report *formatting.ImportsReport)) Option
- func WithOutputPath(path string) Option
- func WithRoot(dir string) Option
- func WithSkipFormat(skipped bool) Option
- func WithSkipFormatFunc(skip func(target string) bool) Option
- func WithTemplates(templates *repo.Repository) Option
- type TidyOption
- func WithGoCommand(command string) TidyOption
- func WithTidyCompat(version string) TidyOption
- func WithTidyEnv(vars ...string) TidyOption
- func WithTidyGoVersion(version string) TidyOption
- func WithTidyOutput(w io.Writer) TidyOption
- func WithTidyWaitDelay(delay time.Duration) TidyOption
- func WithWorkspace(enabled bool) TidyOption
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Error ¶
type Error string
Error is a string that implements error, so a sentinel below can be a constant.
const ErrGenApp Error = "code generation error"
ErrGenApp matches every error this package returns.
It is attached where an error from elsewhere crosses into this package — from the templates repository, from text/template, from the formatter, from os — and a call that already wrapped one passes it back untouched. Attaching it twice would say "code generation error" twice in one message and add nothing the first said; TestErrorsWrapOnce counts it on every path.
const ErrNoModule Error = "no module and no GOPATH covers the output path"
ErrNoModule is returned when nothing above the output path declares a module, and the path is under no GOPATH either.
Generated code lands where the caller says, and that may be outside any module: a fresh directory in /tmp, a tree beside a repository rather than inside it. GoGenApp.ModuleRequired asks the same question without treating the answer as a failure.
type GoGenApp ¶
type GoGenApp struct {
// contains filtered or unexported fields
}
GoGenApp renders templates into formatted Go.
It holds a templates repository and the formatting settings, and does the same three things for every file a generator produces: execute a template, format what it produced, write it.
func New ¶
New builds a GoGenApp rendering from the repository WithTemplates gives it.
It returns an error when no repository was given. Everything a repository is made of — its sources, its funcmap, the roots it is scoped to — is settled by github.com/go-openapi/codegen/templates-repo.New, and a repository that would not build has already reported why by the time it reaches here.
func (*GoGenApp) EnclosingModule ¶
EnclosingModule finds the module the output path belongs to.
It returns the module path as go.mod declares it, and the directory that go.mod sits in. The search walks up from the output path and stops at the first go.mod, so a module nested inside another wins, which is how the go command reads the same tree.
It reads go.mod files and nothing else: no go command, no environment, no GOPATH. A directory that does not exist yet is not an obstacle, since only its name takes part in the answer.
It returns ErrNoModule when the walk reaches the root of the file system without finding one.
func (*GoGenApp) InitModule ¶
InitModule writes a go.mod in the output path, as "go mod init" would.
It writes the module path, the go directive, the toolchain directive when WithToolchain asks for one, and whatever WithRequire declared, formatted the way the go command formats a go.mod. It runs no command and reads no environment, so a generator can lay down a buildable module on a machine with no Go toolchain installed, and the file it produces does not depend on the one that is installed.
What it does not do is resolve anything. "go mod init" fills nothing in either; the versions a module ends up with come from "go mod tidy", which needs the toolchain and the network. Declare what the templates import with WithRequire and tidy has somewhere to start.
A go.mod already in the output path is left alone and reported as fs.ErrExist, unless WithReplaceExisting says otherwise.
func (*GoGenApp) ModuleRequired ¶
ModuleRequired reports whether the output path needs a go.mod of its own.
It is true when nothing above the output path declares a module, which is when generated code there could not be built until GoGenApp.InitModule gives it one.
It is false when a module already covers the path. That module may be the one the generator is running from, so a caller generating into its own repository gets false and needs no go.mod.
GOPATH does not enter into it, though GoGenApp.PackagePath falls back to it. A tree under GOPATH/src builds only with GO111MODULE off, and go has defaulted the other way since 1.16, so such a tree does need a go.mod for the go a caller will be running.
func (*GoGenApp) PackagePath ¶
PackagePath returns the import path of the output path.
It is the module path of the enclosing module followed by the way down to the output path, so a generator can write the import statements that reach the code it is about to produce:
module example.com/petstore declared in /src/petstore/go.mod output path /src/petstore/gen/models PackagePath example.com/petstore/gen/models
A caller generating into a tree that has no module yet calls GoGenApp.InitModule first, and PackagePath then returns what that declared.
Failing a module, a path under GOPATH/src answers too: the way down from src is the import path such a tree has when GO111MODULE is off. Modules are looked for first, since they win wherever both apply.
It returns ErrNoModule when neither names the path. See GoGenApp.ModuleRequired.
func (*GoGenApp) Render ¶
Render executes a template and writes the formatted result to w.
The repository knows each template by a name derived from its asset path; see github.com/go-openapi/codegen/templates-repo.
Render formats unless WithSkipFormat is set, so it is the entry point for Go. Use GoGenApp.RenderFile, which decides by target name, for a generator writing Go and other things side by side.
A template rendering Go that does not parse leaves w untouched: the formatter reads the whole source before it writes anything.
func (*GoGenApp) RenderFile ¶
RenderFile executes a template and writes the result to target, under the output path.
It creates the directories the target needs. A target ending in ".go" is formatted; anything else is written as rendered. See WithSkipFormatFunc and WithSkipFormat.
The file appears whole or not at all: RenderFile writes beside the target and renames over it, so a template that fails to render or to format leaves whatever was there untouched, and a write that fails halfway leaves no half-written target.
When the formatter rejects what a template rendered, the unformatted output is kept beside the target, named for it with a ".unformatted" suffix, and the error names the file. A parse error reports a line and a column, and reading them means reading the source they came from; that source would otherwise be gone.
func (*GoGenApp) Templates ¶
func (g *GoGenApp) Templates() *repo.Repository
Templates returns the repository the app renders from, for a caller wanting to document, audit or derive it.
func (*GoGenApp) TidyModule ¶
func (g *GoGenApp) TidyModule(ctx context.Context, opts ...TidyOption) error
TidyModule runs "go mod tidy" in the output path.
This is the one thing in this package that needs a Go toolchain. Tidying resolves every import the generated code makes against the module graph and the checksum database, downloading what it must, and reproducing that here would mean reproducing the go command. So it is shelled out, and a generator that never calls it never needs go on the machine.
The context bounds the run: cancelling it kills the command, and WithTidyWaitDelay bounds how long the command may hold the output pipes open after that.
A go.work in a parent directory that does not list the generated module makes the go command refuse to work in it, so the command runs with GOWORK off unless WithWorkspace says otherwise. WithTidyEnv sets anything else the command needs, such as GOPROXY or GOPRIVATE.
What the command wrote is reported when it fails; pass WithTidyOutput to watch it as it runs.
type ModOption ¶
type ModOption func(modOptions) modOptions
ModOption configures the go.mod GoGenApp.InitModule writes.
func WithGoVersion ¶
WithGoVersion sets the go directive, as in "1.25.0".
It defaults to the version of Go this program was built with, which is what "go mod init" writes.
func WithModulePath ¶
WithModulePath names the module, as the argument to "go mod init" does.
The path is cleaned and slash-separated, and it is checked the way the go command checks it, so a path no module could have is reported here rather than by the first build.
func WithReplaceExisting ¶
WithReplaceExisting overwrites a go.mod that is already there.
Without it GoGenApp.InitModule leaves an existing file alone and reports fs.ErrExist, as "go mod init" does.
func WithRequire ¶
WithRequire adds a require directive, as in ("github.com/go-openapi/strfmt", "v0.24.0").
A generated module knows what its templates import, and saying so here means "go mod tidy" has versions to start from rather than resolving every import from scratch. Mark a requirement indirect when nothing the module itself holds imports it.
func WithToolchain ¶
WithToolchain sets the toolchain directive, as in "go1.25.0", or "default" to pin the module to whatever toolchain is installed.
The two directives are spelled differently — "go 1.25.0" carries no prefix, "toolchain go1.25.0" does — so a bare version is accepted here and written in the form the directive takes.
There is no default: "go mod init" writes no toolchain line, and the go command adds one when it needs a toolchain newer than the one installed. Set it to say which toolchain a generated module is meant to build with, whatever is on the machine that generated it.
type Option ¶
type Option func(options) options
Option configures a GoGenApp.
func WithFormatOptions ¶
func WithFormatOptions(opts ...formatting.Option) Option
WithFormatOptions configures the formatter.
Grouping, gofumpt and the rest are settled by github.com/go-openapi/codegen/formatting, and this package re-exports none of it:
genapp.WithFormatOptions(
formatting.WithImportGroups("github.com/go-openapi", baseImport),
)
func WithImportsReporter ¶
func WithImportsReporter(report func(template string, report *formatting.ImportsReport)) Option
WithImportsReporter calls report for every file rendered, with the name of the template that rendered it.
formatting.Format keeps an import whose package it cannot name, rather than delete one the code may be using, and says so in the report. Use this to see those:
genapp.WithImportsReporter(func(template string, report *formatting.ImportsReport) {
if report.HasImportsInDoubt() {
log.Printf("%s: %v", template, report.PathsInDoubt())
}
})
Resolve the paths it lists once, then pass the names through formatting.WithResolvedImports with WithFormatOptions.
func WithOutputPath ¶
WithOutputPath sets where GoGenApp.RenderFile writes. Targets are relative to that directory.
func WithRoot ¶
WithRoot confines every file a GoGenApp writes to dir.
The output path must sit at or below dir, and dir must exist. The caller declares the root, so WithRoot reports a missing one instead of creating it. Nothing outside dir is written, whether a target climbs out with "..", names an absolute path, or reaches a symbolic link pointing away. os.Root checks each link as it walks the path, where a prefix test on the name alone would miss a link halfway down.
app, err := genapp.New(
genapp.WithTemplates(templates),
genapp.WithOutputPath("./gen/models"),
genapp.WithRoot("./gen"),
)
Use it when a spec supplies the target names, such as its operation and model names.
Without it, writes still stay under the output path and the checks on a target still run. WithRoot widens the boundary past the output path, for a generator writing into several directories of one tree.
It covers this package's writes and no more. GoGenApp.TidyModule runs the go command, which writes go.mod and go.sum itself, and no root reaches into another process. Reads run unconfined too: GoGenApp.PackagePath and GoGenApp.EnclosingModule walk up from the output path looking for a go.mod, and that go.mod usually sits above any root worth setting.
os.Root confines path resolution and no more. It does not stop traversal of a bind mount, a /proc special file or a device file, so point WithRoot at a directory that holds only generated output.
func WithSkipFormat ¶
WithSkipFormat writes every target as the template rendered it.
A template that produces Go which does not parse makes GoGenApp.RenderFile fail with nothing written, and finding out why means reading the output. Turn this on and the file lands unformatted.
func WithSkipFormatFunc ¶
WithSkipFormatFunc decides which targets GoGenApp.RenderFile formats.
The default formats a target whose name ends in ".go" and copies anything else through.
func WithTemplates ¶
func WithTemplates(templates *repo.Repository) Option
WithTemplates sets the repository to render from. New needs it.
The repository is built by github.com/go-openapi/codegen/templates-repo.New, which is where the sources, the funcmap and the scoping are decided:
templates, err := repo.New(
repo.FromFS(assets, ""),
repo.WithFuncMap(golang.FuncMap(mangling.MakeGoMangler())),
)
if err != nil {
return err
}
app, err := genapp.New(genapp.WithTemplates(templates))
type TidyOption ¶
type TidyOption func(tidyOptions) tidyOptions
TidyOption configures GoGenApp.TidyModule.
func WithGoCommand ¶
func WithGoCommand(command string) TidyOption
WithGoCommand names the go binary to run. It defaults to "go", found on PATH.
Pass an absolute path to run a toolchain the PATH does not point at.
func WithTidyCompat ¶
func WithTidyCompat(version string) TidyOption
WithTidyCompat passes -compat to the command, as in "1.24", which keeps the checksums an older go needs to load the module.
func WithTidyEnv ¶
func WithTidyEnv(vars ...string) TidyOption
WithTidyEnv sets environment variables for the command, as "GOPROXY=off" or "GOPRIVATE=example.com".
They are added to the environment this process runs in, so a later setting replaces an earlier one. Tidying reaches the module proxy and the checksum database, and a generated module often wants different settings for those than the generator itself.
func WithTidyGoVersion ¶
func WithTidyGoVersion(version string) TidyOption
WithTidyGoVersion passes -go to the command, as in "1.25.0", which sets the go directive while tidying.
func WithTidyOutput ¶
func WithTidyOutput(w io.Writer) TidyOption
WithTidyOutput copies what the command writes to w, as it writes it.
The output is kept either way and reported when the command fails. Pass a writer to watch a tidy that takes a while, since it downloads what the module requires.
func WithTidyWaitDelay ¶
func WithTidyWaitDelay(delay time.Duration) TidyOption
WithTidyWaitDelay bounds how long the command may hold the output pipes open after its context is done, before it is killed. It defaults to five seconds.
func WithWorkspace ¶
func WithWorkspace(enabled bool) TidyOption
WithWorkspace lets the go workspace apply to the command.
A generated module is usually a module of its own, and a go.work in a parent directory that does not list it makes the go command refuse to work in it, so GoGenApp.TidyModule runs with GOWORK off. Turn this on for a module the surrounding workspace is meant to cover.