Documentation
¶
Overview ¶
Package gen defines what a generator is and how rig runs one.
A generator is a pure function from the frozen document to a set of files. Purity is the whole design: because a generator produces its output in memory rather than writing as it goes, `rig check` can run every generator and compare the result to disk without a single special case per generator, and two runs over the same document can be diffed to prove nothing changed.
Generators register themselves. Adding one touches exactly one place — its own init — rather than a switch, a config struct, and a diff function that all have to be kept in step.
Index ¶
- Constants
- Variables
- func Decode[T any](o Options) (T, error)
- func NeedsWork(deltas []Delta) bool
- func Register(g Generator)
- func Report(w io.Writer, root string, deltas []Delta, verbose bool)
- func Sum(content []byte) string
- type Artifact
- type Delta
- type DiffOptions
- type Entry
- type Generator
- type Manifest
- type Options
- type Registry
- type Result
- type Spec
- type Status
- type WriteMode
- type WriteOptions
Constants ¶
const Banner = "// Code generated by rig. DO NOT EDIT."
Banner is the first line of every file rig owns outright.
It is written by the emitters and read back by Orphans: a file carrying it is rig's, whoever last touched it. Both halves come from here so they cannot drift apart — a scan looking for a line the emitters stopped writing would quietly find nothing.
const ManifestName = ".rig/manifest.json"
ManifestName is where the manifest lives, relative to the project root.
Variables ¶
var Default = NewRegistry()
Default is the registry the CLI uses. Built-in generators add themselves to it from their own init.
var Retired = map[string]string{
"electric": "live-sync shapes are written by server-go now, into the API " +
"package beside the routes they answer next to. Delete this block and move " +
"its `electric_url` and `stub_dir` options onto server-go, adding " +
"`api_import` with the same value service-go has. The `shape_import` " +
"option is gone: a scoping stub names the API package it is already beside.",
}
Retired names a generator that used to exist, and says what to do instead.
A configuration is a file somebody wrote by hand and does not reread, so a generator that goes away has to answer for itself once. Without this the message is "no generator named electric", which is true and tells a reader nothing about where their shape endpoints went.
Functions ¶
func Decode ¶
Decode reads the options block into a typed struct.
Unknown keys are an error: a mistyped option that is silently ignored looks configured and behaves as though it is not, which is the worst of both.
func NeedsWork ¶
NeedsWork reports whether any delta represents a difference, which is what `rig check` exits non-zero on.
func Register ¶
func Register(g Generator)
Register adds a generator to the default registry.
A duplicate name panics at init rather than failing later: two generators answering to one name is a build mistake, and finding out at startup beats finding out from whichever one happened to register second.
Types ¶
type Artifact ¶
type Artifact struct {
// Path is relative to the generator's output directory.
Path string
// Content is the complete file.
Content []byte
Mode WriteMode
// Perm defaults to 0644 when zero.
Perm fs.FileMode
}
Artifact is one file a generator produced.
The content is fully materialized rather than streamed. That is what makes a generator testable without a filesystem and `check` possible without running a second, parallel implementation of every generator.
type Delta ¶
type Delta struct {
Generator string
Path string
Status Status
// Reason explains a conflict.
Reason string
// contains filtered or unexported fields
}
Delta is one file's worth of difference between what a generator produced and what is on disk.
func Diff ¶
Diff compares generated artifacts against the filesystem.
Three things can be wrong with a file, and they are found three ways. What a generator produces now is compared against what is on disk. What the manifest records is what makes a hand edit distinguishable from output rig has not caught up with. And what is on disk under rig's own naming — see Orphans — is what makes a leftover visible in a checkout that has no manifest, which is every checkout CI makes.
func Orphans ¶
Orphans finds files on disk that rig wrote and no generator claims any more.
The manifest cannot answer this on its own. It is gitignored by default, so a clean checkout — which is what CI is — has no record of what rig wrote there. What does survive a clone is the output itself: the `.gen.` in a name and Banner on the first line are rig's claim on a file, and a claim no generator renews is a leftover. A renamed table that left its old file behind is caught on a machine that has never generated anything.
The two signals are both needed. The OpenAPI documents carry no banner, because YAML that begins with a comment is not what anyone wants to read; the TypeScript client's index.ts carries one but cannot be renamed, because a package entry point is called index.ts. Neither signal covers a hand-owned stub, which is the point: a stub has no banner and no `.gen.`, so it is never mistaken for rig's.
type DiffOptions ¶
type DiffOptions struct {
// Partial says some of the configured generators were held back, as --only
// does.
//
// Staleness is relative to the run. A generator that did not run still owns
// every file it wrote, and calling those leftovers would turn a narrow
// regeneration into a proposal to delete most of the project. Which
// generators ran is read from the results; that a generator was held back
// is the one thing the results cannot say, because a generator nobody asked
// leaves nothing behind to be missing from.
Partial bool
}
DiffOptions scope what a comparison is entitled to call stale.
type Entry ¶
type Entry struct {
// Path is relative to the project root, in slash form, so a manifest
// committed on one machine reads correctly on another.
Path string `json:"path"`
Generator string `json:"generator"`
Mode string `json:"mode"`
SHA256 string `json:"sha256"`
}
Entry is one recorded file.
type Generator ¶
type Generator interface {
// Name is the registry key, and what rig.yaml refers to.
Name() string
// Description is one line, shown by `rig generators`.
Description() string
// Version changes when the output format changes, so a stale manifest can
// be recognized as stale.
Version() string
// Generate must be pure: the same document and options must always produce
// byte-identical artifacts. No filesystem, no clock, no network, and no
// iteration over a map without sorting it first.
//
// Whatever the output format can say about a field, it says with the
// document's own words: ir.Field.Description is the single copy of the
// human-readable text, and an output that has somewhere to put one — a doc
// comment, an OpenAPI description, a JSDoc block — puts that one there
// rather than a paraphrase of it.
Generate(ctx context.Context, doc *ir.Document, opts Options) ([]Artifact, error)
}
Generator turns a document into files.
type Manifest ¶
type Manifest struct {
// Version is the manifest format, not the tool's.
Version int `json:"version"`
// IRHash identifies the document the recorded files were generated from.
IRHash string `json:"ir_hash,omitempty"`
// Generators maps a generator name to the version that last ran.
Generators map[string]string `json:"generators,omitempty"`
// Files is every file rig wrote, ordered by path.
Files []Entry `json:"files"`
}
Manifest records what rig wrote, so that a later run can tell its own output from someone else's edits.
It is not a cache. Generation always runs; the manifest exists to answer two questions a cache would not: is this file still claimed by a generator, and has anyone changed it since rig wrote it.
func LoadManifest ¶
LoadManifest reads the manifest for a project. A missing manifest is not an error: the first run has nothing to remember.
func Write ¶
Write applies the deltas and returns the manifest describing the result.
A conflict stops the whole run before anything is written. Applying half a generation and then refusing the rest would leave the tree in a state neither the tool nor the developer can reason about.
type Options ¶
type Options struct {
// OutDir is where the artifacts go, resolved to an absolute path.
OutDir string
// Root is the project root.
//
// Almost everything a generator writes belongs under OutDir. The exception
// is a hand-owned scaffold, which belongs where the developer works rather
// than beside the code it implements — so a generator that emits one needs
// to know where the project starts.
Root string
// Raw is this generator's own options block from rig.yaml.
Raw map[string]any
}
Options are what a generator is configured with.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry holds the known generators.
func (*Registry) MustRegister ¶
MustRegister adds a generator, panicking on a duplicate name.
type Result ¶
Result is what one generator produced. Paths are absolute by this point.
func Run ¶
func Run(ctx context.Context, r *Registry, doc *ir.Document, root string, specs []Spec) ([]Result, error)
Run executes the requested generators against a document.
A document that failed validation is refused outright. Generating from a schema rig knows to be wrong would produce code that compiles and misbehaves, which is far worse than producing nothing.
type Status ¶
type Status string
Status is what would happen, or did happen, to one file.
const ( // Added is a file that does not exist yet. Added Status = "add" // Changed is a generated file whose content differs. Changed Status = "change" // Unchanged is already correct. Unchanged Status = "ok" // Stale is a file rig wrote that no generator claims any more, left behind // by a renamed table or a removed generator. Stale Status = "stale" // Conflict is a generated file someone has edited by hand. Overwriting it // would throw that work away without asking. Conflict Status = "conflict" // Kept is a create-once file that already exists and is left alone. Kept Status = "keep" )
type WriteMode ¶
type WriteMode int
WriteMode says who owns a generated file.
const ( // Overwrite is rewritten on every run. Such a file is gitignored, excluded // from linting, and never edited by hand. Overwrite WriteMode = iota // CreateOnce is written once at its final name and never touched again. // // There is no rename step: a scaffold that needs a second command before it // is usable is a papercut, and the manifest remembers what was written well // enough to leave it alone. CreateOnce )
type WriteOptions ¶
type WriteOptions struct {
// Force overwrites files that were edited by hand.
Force bool
// Prune deletes files no generator claims any more.
Prune bool
// Previous is the manifest this run started from, if there was one.
//
// The new manifest is built from this run's deltas, and a run narrowed by
// --only has no deltas for the generators it held back. Without their old
// entries carried across, a narrow regeneration would cost every other
// generator's files the one record that tells a hand edit from output rig
// has not caught up with.
Previous *Manifest
}
WriteOptions tune how deltas are applied.