❯ rotini --documentation█

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 specIn zz_rotini.go
each command’s flagsa struct: TodoAddFlags { Priority string; Tag []string }
each command’s argumentsa struct: TodoAddArguments { Title string }
its env: and config: inputs, when it declares anyTodoAddEnv and TodoAddConfig structs, reached as in.TodoAdd.Env and in.TodoAdd.Config
its stdin: document, when it declares onea TodoAddStdin struct
its output:, when it declares onea 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 parentsTodoAddInputs, the type your handler passes to rtx.Inputs
the command treea rotini.Definition the runtime parses against
every commanda method on the ProgramHandlers interface, which returns the command’s handler (todoAddHandler)
configuration files and the env prefixInputSettings, which tells the runtime where values come from
the conf’s generate.features:Help(path...), and Completion(shell), Man(path...) and Markdown(path...) when those features are on
every command’s usage lineUsage(path...), generated whether or not the help feature is on

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 {
	Priority string   `rotini:"priority"`
	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(definition, 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.

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 next go generate disables its handler file by adding //go:build ignore at 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 its var _ rotini.Handler line, or list it under the conf’s keep.
  • 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_dir with template: 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.

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.