sxcli.devSimple Extensible CLI
You are viewing an archived snapshot (v0.2.0). Switch to the current version →

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, value domains the framework enforces on every source, and advisory hints about what a value denotes. Same file server, grown up.

The applet, annotated#

go
go
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{
                "Dir": fw.FieldMetadata[string]{
                    Hint: fw.HintDirectory, // advisory: tooling completes paths
                    Doc:  "Document root served over HTTP.",
                },
                "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()
}

Three kinds of annotation, per field:

  • Doc — the long-form description. The usage: tag stays the --help one-liner; Doc is 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.
  • Hint — advisory, never enforced: --dir names a directory, so tooling can complete paths there. The framework will not check that the directory exists (that is the applet's job, with a better error) — a hint says what a value denotes, not what it must be. Allowed and Hint are mutually exclusive on one field: a closed enum and "it's a path" contradict each other.

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:

sh
sh
$ 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:

sh
sh
$ 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 with ArgInfo.Allowed and ArgInfo.Hint populated — a completion service offers exactly list, none, spa after --index (and can trust the set, because the machinery enforces it), and switches to directory completion after --dir.

The full rules live in Your config struct § Metadata and value domains.