zz_rotini.go
go generate ./... turns your spec into one Go file, internal/cmd/<name>/zz_rotini.go. It is
rewritten on every run, so never edit it. This page explains what is in it and which files around
it are yours.
From spec to code
For a spec with a todo root command and an add sub-command, the file holds:
| In the spec | In zz_rotini.go |
|---|---|
| each command’s flags | a struct: TodoAddFlags { Priority string; Tag []string } |
| each command’s arguments | a struct: TodoAddArguments { Title string } |
its env: and config: inputs, when it declares any | TodoAddEnv and TodoAddConfig structs, reached as in.TodoAdd.Env and in.TodoAdd.Config |
its stdin: document, when it declares one | a TodoAddStdin struct |
its output:, when it declares one | a TodoAddOutput type (see structured output) |
each schema under the root’s schemas: | a Go type of the same name, such as Task, with json tags; a property not listed under required: gets omitempty |
| the command and its parents | TodoAddInputs, the type your handler passes to rtx.Inputs |
| the command tree | a rotini.Definition the runtime parses against |
| every command | a method on the ProgramHandlers interface, which returns the command’s handler (todoAddHandler) |
| configuration files and the env prefix | InputSettings, which tells the runtime where values come from |
the conf’s generate.features: | Help(path...), and CompletionScript(shell), Man(path...) and Markdown(path...) when those features are on |
| every command’s usage line | Usage(path...), generated whether or not the help feature is on |
| each enum value of a string input | a constant: TodoAddPriorityHigh = "high", its summary as the doc comment |
each value of a string schema under schemas: | a constant of the schema’s type: StatusOpen Status = "open" |
the root’s summary | the package’s doc comment, unless a file of yours in the package has one |
| each configuration file, config key, profile and environment variable | a constant: TodoConfigFileUser = "user", TodoConfigKeyServerPort = "server.port", TodoProfileProd = "prod", TodoEnvToken = "TODO_TOKEN" |
It ends with what main.go and your tests use: NewProgram(), which builds the program with the
generated handlers, and NewProgramWith(handlers), which builds it with a set you pass, to swap
or wrap a command’s handler. Handlers() returns the generated set, for NewProgramWith to
embed. main.go builds its program with NewProgram(); nothing is built at package init.
zz_rotini.go (abridged)
// Code generated by rotini; DO NOT EDIT.
package todo
type TodoAddFlags struct {
// how urgent it is
Priority string `rotini:"priority"`
// a label to file it under
Tag []string `rotini:"tag"`
}
type TodoAddArguments struct {
Title string `rotini:"title"`
}
type TodoAddCommandInputs struct {
Flags TodoAddFlags
Arguments TodoAddArguments
}
type TodoAddInputs struct {
Todo TodoCommandInputs
TodoAdd TodoAddCommandInputs
}
func NewProgram() *rotini.Program {
return NewProgramWith(&handlers{})
}
func NewProgramWith(handlers ProgramHandlers) *rotini.Program {
return rotini.NewWiredProgram(rotiniDefinition(), func(name string) (rotini.Handler, bool) {
switch name {
case "Todo":
return handlers.Todo(), true
case "TodoAdd":
return handlers.TodoAdd(), true
}
return nil, false
},
rotini.Env(),
rotini.ConfigFiles(),
rotini.YAML(),
).
WithInputSettings(InputSettings).
WithHelp(Help)
}
func Handlers() ProgramHandlers {
return &handlers{}
}A handler reads its own values from in.TodoAdd and a parent’s flags from in.Todo. The types
generated from schemas: are ordinary Go types, so your own code, such as a store that saves
tasks, can use them too. The generated handler set must implement ProgramHandlers, so the
compiler reports a command that has no handler. The switch finds each command’s handler by name
without reflection, which keeps the binary smaller.
To replace one command’s handler, for a test double or to wrap it, embed the generated set and override that method:
replacing one handler
type fakeSync struct{ todo.ProgramHandlers }
func (fakeSync) TodoSync() rotini.Handler { return &stubSync{} }
todo.NewProgramWith(fakeSync{todo.Handlers()}).Run(args)A hand-written handler set can do the same with rotini.NewProgramFunc; rotini.NewProgram(def, handlers) finds the methods by reflection.
The list after the switch is the runtime features the spec uses, such as environment inputs,
a YAML configuration file or a url flag. The program links only those, so a small CLI’s
binary is smaller and starts faster. go generate keeps the list in step with the spec; see
what a program links. Env and config fields write their enum
values, aliases and time layouts in their struct tags as comma-separated lists, with \
escaping a ,, = or \ inside a value.
Each input field carries its input’s summary as a doc comment, and a Deprecated: paragraph
when the input is deprecated, so an editor shows both and flags a use. The constants let a
handler name a value once: in.TodoAdd.Flags.Priority == TodoAddPriorityHigh, or
rotini.SetConfigValue(rtx, TodoConfigFileUser, TodoConfigKeyServerPort, raw). A value renamed
in the spec then stops compiling instead of silently never matching. A constant is named after
its command (or flag set, or schema), the input and the value. When two spellings make one name
(dry-run and dry_run), or the name is taken, rotini validate warns and that constant is
left out; one that a file of yours already declares is left to you. go generate also warns
about a file, key or profile name written into a rotini.SetConfigValue, UnsetConfigValue,
ConfigFilePath, ConfigProfiles or ConfigToProfile call that the spec doesn’t declare.
To put the input and output types in a package of their own, for example so a handler in
another package can import them, declare a models target under the conf’s
packages.
What stays yours
- The handler files, one per command, such as
todo_add.go. Each is created the first time its command appears in the spec and is never overwritten. When you remove a command from the spec, the nextgo generatedisables its handler file by adding//go:build ignoreat the top, so it is out of the build but your code is still there, and the one after deletes it, edits and all. Each step is reported. To keep the file, delete itsvar _ rotini.HandlerE(orrotini.Handler) line, or list it under the conf’skeep. main.go, created once and never touched again.- Any other file you add to the package, such as a test or a file declaring a dependency. Rotini only removes files it wrote.
- An editable template a feature seeds into its
template_dirwithtemplate: true, written once when missing and never touched again.
Renaming a command, or changing its filename:, is a removal and an addition: the old handler
file is disabled and then deleted, and a new, empty one is created. Move your code across
before the second go generate, or recover it from git afterwards. A rename that changes
only letter case (Deploy to deploy) on a case-insensitive file system, as on macOS and
Windows, keeps the handler file under its old spelling, since both names are the same file.
What rotini rewrites on every run, and never asks you to edit:
- the generated file, and the models file when the conf declares one;
- each feature’s rendered pages under its
embed_dir(removing pages for commands that are gone); - the JSON Schema copies named under
generate.schemas, which are never removed.
Nothing is written outside the paths your conf names.
Names
Generated names follow Go’s conventions: api-group becomes APIGroup, and base-url becomes
BaseURL. A type’s prefix is the command path, so todo add gives TodoAdd... types and the
handler type todoAddHandler. A command named port-forward gets the handler file
todo_port_forward.go.