Documentation
¶
Overview ¶
Package docsgen renders reference documentation from the things that define it: the cobra command trees, and the configuration schema.
It lives in the kernel because a generator over a command tree is MECHANISM, and gapi is the mechanism repo. magelib cannot hold it - magelib requires exactly one module, mage, and teaching a build library what cobra is would be the wrong trade. goblin cannot hold it either, since goblin already resolves its entire configuration schema from core/config. So the placement follows the silo's own mechanism/policy line rather than cutting across it.
Nothing here reaches for a clock. Every artifact this package writes is committed and byte-compared by magelib's drift gate, so a timestamp anywhere would make every build dirty. Dates come from the repo VERSION, passed in by the caller.
All renderers are pure functions over in-memory models, which is what lets them be tested against a synthetic cobra tree and a synthetic struct rather than against gapi's real ones.
Index ¶
- func CLI(root *cobra.Command, dir string) error
- func ConfigMan(m *ConfigModel, date string) []byte
- func ConfigMarkdown(m *ConfigModel, weight int) []byte
- func DefaultsJSON(m *ConfigModel, source string) ([]byte, error)
- func Man(root *cobra.Command, dir, version string) error
- func Overview(srcMarkdown, outRoff, product, version string) error
- type ConfigModel
- type ConfigOptions
- type DefaultEntry
- type Key
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CLI ¶
CLI renders a command tree as relearn-ready markdown, one file per command, under dir.
THE ROOT PASSED HERE MUST BE THE POPULATED ONE. gapictl has two: a package-level singleton that five func init() blocks add verbs to, and NewGapictlRoot, which returns a fresh root carrying only the persistent flags and `version`. Measured: the singleton walks to 25 commands and the constructor to 2. Generating from the constructor produces a reference that omits ping, shutdown, tui, and every agent, crypto and lifecycle verb - and a drift gate over that output stays green forever, because it is comparing a stub to itself. Use cli.GetRoot(); see tools/gendocs.
DisableAutoGenTag is set because cobra's tag carries a date. Every artifact here is committed and byte-compared, so one clock anywhere makes every build dirty.
func ConfigMan ¶
func ConfigMan(m *ConfigModel, date string) []byte
ConfigMan renders <product>.conf.5 as roff.
Emitted directly rather than converted from the markdown above. go-md2man is vendored here and handles prose well, but a definition list is not standard markdown, and round-tripping structured data through a prose format to recover structure is what produced the roff-shaped homepage this whole programme is replacing.
date is the repo VERSION, never time.Now(): these pages are committed and byte-compared, so a clock would make every build dirty.
func ConfigMarkdown ¶
func ConfigMarkdown(m *ConfigModel, weight int) []byte
ConfigMarkdown renders the model as the configuration reference page.
func DefaultsJSON ¶
func DefaultsJSON(m *ConfigModel, source string) ([]byte, error)
DefaultsJSON renders the model as the data file Hugo's `default` shortcode reads and magelib's defaults gate scans.
Prose stops transcribing values because of this file: a page writes {{< default "transport.address" >}} and Hugo fails the build on an unknown key, so a stale quotation cannot survive a rename.
func Man ¶
Man renders section 1 pages for a command tree into dir.
version is the repo VERSION and becomes the page's date.
THE DATE IS THE ONLY TRAP IN SECTION 1, and it is silent. Cobra's GenManHeader stamps time.Now() when Date is nil, so every regeneration produces different bytes and a committed man page is dirty the moment anyone rebuilds - which under a byte-comparing drift gate reads as "the CLI changed" on a tree where nothing changed. Passing a date derived from VERSION makes the page a function of the source, which is what the gate assumes it already is.
A man page's date field conventionally carries a date rather than a version, and this deliberately does not. The field's job here is to identify WHICH BUILD the page describes, and a version answers that where a build date answers when someone happened to run the generator.
func Overview ¶
Overview converts a hand-written markdown page into a section 7 roff page.
Section 7 is the one place in the reference surface that is WRITTEN rather than generated: "what is this system and how do its parts fit" is not derivable from a command tree, and pretending otherwise is what produced today's Docs:Man, which runs a prose converter over docs/index.md and emits a roff-shaped homepage.
The conversion goes through go-md2man, which is already vendored here because cobra's own man generator uses it. That is what lets pandoc leave both flakes: one roff path serves sections 1, 5 and 7, and the toolchain loses a system binary rather than gaining a purpose for it.
Types ¶
type ConfigModel ¶
type ConfigModel struct {
// Product is "gapi" or "goblin".
Product string
// Keys are sorted by Path. Sorted rather than in declaration order
// because a map is walked somewhere upstream of every renderer, and
// the drift gate compares bytes: an unstable order would make every
// second build dirty for no reason.
Keys []Key
}
ConfigModel is the whole schema for one product.
func BuildConfigModel ¶
func BuildConfigModel(o ConfigOptions) (*ConfigModel, error)
BuildConfigModel joins the reflection walk, the ast walk and the defaults viper into one model.
The three sources are separate because each knows something the others cannot: reflection knows the key paths and Go types, the ast knows the doc comments, and the viper knows the resolved product-aware values. A renderer reading any one of them alone produces a page that is correct about a third of what it says.
type ConfigOptions ¶
type ConfigOptions struct {
// Product is the product name the model is being built for.
Product string
// Schema is a zero value of the configuration struct, e.g.
// &config.Config{}.
Schema any
// Defaults is the viper carrying every registered default, i.e.
// config.Defaults().
Defaults *viper.Viper
// SourceDir is the directory holding the schema's Go source, read for
// doc comments. Empty means no comments are joined.
SourceDir string
// EnvKeyFor renders a config path as its environment variable. It is
// a function rather than a derived string because the composition
// belongs to core/config, which owns the guarantee that the name it
// prints is the name that works.
EnvKeyFor func(path string) string
}
ConfigOptions describes where each part of the model comes from.
Every source is injected rather than reached for, which is what lets the renderers be tested against a synthetic struct and a synthetic viper instead of against gapi's real configuration - the difference between testing this package and testing core/config through it.
type DefaultEntry ¶
type DefaultEntry struct {
Value string `json:"value"`
Env string `json:"env"`
Type string `json:"type"`
Source string `json:"source"`
}
DefaultEntry is one key as published in defaults.json.
The field names match magelib's reader exactly. They are the contract between the generator and the gate: the gate scans documents for these values, and a rename on one side without the other produces a gate that silently checks nothing.
type Key ¶
type Key struct {
// Path is the dotted config path, e.g. "transport.address".
Path string
// Type is the Go type name of the leaf, e.g. "string".
Type string
// Doc is the field's doc comment. Reflection cannot see comments, so
// this comes from an ast walk and is empty when the field has none.
Doc string
// Env is the environment variable that overrides this key.
Env string
// Value is the resolved default, rendered as a string. Product-aware:
// the same key yields a different value under gapi and goblin.
Value string
// Struct and Field name where the key comes from, so the ast walk can
// be joined without guessing and a reader can find the declaration.
Struct string
Field string
}
Key is one operator-settable configuration key, joined from the three places that each know part of it.