Advanced CLI
The minimal CLI stopped at struct tags. This demo
adds the declarative layer on top: fw.WithMetadata — long-form
descriptions for humans and tooling, and value domains the framework
enforces on every source. Same file server, grown up.
The applet, annotated#
package main import ( "log/slog" "net/http" fw "sxcli.dev/fw" _ "sxcli.dev/fw/configfmt/yaml" _ "sxcli.dev/fw/sink/console" ) type Config struct { Addr string `json:"addr" arg:"addr,a" usage:"listen address"` Dir string `json:"dir" arg:"dir,d" usage:"directory to serve"` Index string `json:"index" arg:"index" usage:"directory index mode"` Codecs []string `json:"codecs" arg:"codec" usage:"compression codec, repeatable"` } type Srv struct{ cfg Config } func (s *Srv) Configured() error { return nil } func (s *Srv) Run() int { slog.Info("serving", "dir", s.cfg.Dir, "addr", s.cfg.Addr, "index", s.cfg.Index, "codecs", s.cfg.Codecs) // … same server as the minimal demo … _ = http.ListenAndServe return 0 } func main() { s := &Srv{cfg: Config{Addr: ":8080", Dir: ".", Index: "list"}} fw.Register("srv", s, fw.WithConfig(&s.cfg), fw.WithMetadata(&fw.Metadata{ Description: "Static file server with negotiable directory " + "index behavior and response compression.", Fields: map[string]any{ "Index": fw.FieldMetadata[string]{ Allowed: []string{"list", "none", "spa"}, Doc: "How directory URLs respond: 'list' renders a " + "listing, 'none' returns 404, 'spa' serves " + "index.html for client-side routing.", }, "Codecs": fw.FieldMetadata[string]{ Allowed: []string{"gzip", "zstd", "br"}, Doc: "Compression codecs offered during content " + "negotiation, in preference order.", }, }, })) fw.Main() }
Two kinds of annotation, per field:
Doc— the long-form description. Theusage:tag stays the--helpone-liner;Docis what documentation generators and rich completion UIs show.Allowed— a closed value domain. Note the default ("list") is itself inside the domain: a registered default outside its own declared set is caught at registration, with every other violation.
Enforced, not advisory#
A non-empty Allowed is honored by the machinery on every write
path — the same rule whether the value arrives by argument, environment
or config file:
$ srv --index tree # startup fails: names the source (argument), # the offending value, and the allowed set $ SRV_INDEX=tree srv # same failure, source: environment $ echo 'srv: {index: tree}' > srv-config.yaml && srv # same, source: file
Slice fields are checked per element:
$ srv --codec gzip --codec lzma # fails: 'lzma' not in [gzip zstd br] $ SRV_CODEC=gzip,zstd srv # fine — every element in the domain
The applet keeps whatever checks it wants as defense in depth, but by
the time Configured() runs, s.cfg.Index is guaranteed to be one of
the three declared values — invalid configurations die at startup,
loudly, before any code runs.
Where the declarations surface#
Declare once, and the same metadata feeds everything downstream:
- the framework's own validation, as above;
Describe("srv")on the introspection surface returns the long-form description;Arguments("srv", args)returns the schema withArgInfo.Allowedpopulated — a completion service can offer exactlylist,none,spaafter--index, and trust the set because the machinery enforces it.
The full rules live in Your config struct § Metadata and value domains.