config: path shorthand is an explicit '%' placeholder, not an omitted field
commit c6ea0e9b8ffdfbd60f76e368f81eb772c6b76bc9 · parent 46fd58b7d4
· browse this tree
Ximianth, 2026-07-30 11:14:21 UTC
Review feedback on the previous commit: a bare exhibit key with no value reads like an accident in yaml, not a request. The shorthand for 'path mirrors the exhibit name' is now the explicit placeholder 'path: "%"', and a missing path is a config error again (with the error message teaching the placeholder).
diff --git a/README.md b/README.md index 35961af..2a86879 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ The format is YAML: ```yaml exhibits: moonshot-1: + path: "%" meta/research/some-topic: path: ../elsewhere/some-topic some-repo: @@ -28,8 +29,8 @@ exhibits: - `exhibits` maps URL prefixes to directories. A prefix may contain slashes; the intermediate URL levels then serve synthetic listings that reveal only the mounted subtrees. -- `path` (optional) is the directory to serve. - When omitted, it defaults to the exhibit's own name -- +- `path` (required) is the directory to serve. + The placeholder `"%"` means "same as the exhibit name" -- the common case where the URL space mirrors the directory layout. Relative paths resolve against the config file's directory, which makes a config portable alongside the trees it describes. @@ -37,7 +38,7 @@ exhibits: Trailing slashes on names and paths are accepted and normalized away. - `expose-git` (optional, default false): see below. -A bad config (parse error, unknown key, or zero exhibits) +A bad config (parse error, unknown key, missing path, or zero exhibits) is a startup error: vitrine refuses to run rather than serve the wrong thing. Config changes apply live: vitrine watches the config file diff --git a/config.go b/config.go index 7301c2b..40f4146 100644 --- a/config.go +++ b/config.go @@ -67,12 +67,17 @@ func loadConfig(path string) (*config, error) { if _, dup := normalized[cleanName]; dup { return nil, fmt.Errorf("config %s: exhibit %q: duplicate of %q after trimming slashes", path, name, cleanName) } - // An omitted path means "same as the exhibit name": + // The path "%" is a placeholder meaning "same as the exhibit name": // the near-universal case where the URL prefix mirrors the directory layout // relative to the config file. - if ex.Path == "" { + // (Explicit placeholder rather than an omittable field: + // a bare key with no value reads like an accident in yaml.) + if ex.Path == "%" { ex.Path = cleanName } + if ex.Path == "" { + return nil, fmt.Errorf("config %s: exhibit %q: missing required field %q (use %q to serve the path matching the exhibit name)", path, name, "path", "path: %") + } ex.Path = strings.TrimRight(ex.Path, "/") if ex.Path == "" { // Only "/" (or "///"...) trims to nothing: serving the root filesystem diff --git a/config_test.go b/config_test.go index b748803..4362231 100644 --- a/config_test.go +++ b/config_test.go @@ -71,10 +71,11 @@ exhibits: } } -func TestConfigOmittedPathMeansExhibitName(t *testing.T) { +func TestConfigPercentPathMeansExhibitName(t *testing.T) { p := writeConfig(t, ` exhibits: meta/research/thing: + path: "%" expose-git: true `) cfg, err := loadConfig(p) @@ -87,12 +88,26 @@ exhibits: } } +func TestConfigMissingPathRejected(t *testing.T) { + p := writeConfig(t, ` +exhibits: + demo: + expose-git: true + bare-key: +`) + _, err := loadConfig(p) + if err == nil || !strings.Contains(err.Error(), "path") { + t.Fatalf("want missing-path error, got %v", err) + } +} + func TestConfigTrailingSlashesNormalized(t *testing.T) { p := writeConfig(t, ` exhibits: demo/: path: /srv/demo/ shorthand/: + path: "%" `) cfg, err := loadConfig(p) if err != nil {