From 45af24b2b151d759bddfba7cc8d8c3e8590a2fd1 Mon Sep 17 00:00:00 2001 From: ZDDC Date: Thu, 4 Jun 2026 10:01:31 -0500 Subject: [PATCH] feat(server): route no-slash directory URLs through views.dir (cascade spine) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit serveSpecializedNoSlash now consults zddc.ViewAt(dir, "dir"): an explicit `views.dir` in the cascade overrides the default_tool-derived app for the no-slash directory URL. default_tool stays the sugar fallback (ViewAt returns it when no views.dir is declared), so existing deployments are unaffected — purely additive. Also fixes the mergeOverlay trap (per the .zddc-policy-key checklist): added Views to walker.go's per-level merge so views: survives cascade resolution at default-driven paths (without it the key silently no-ops). Verified by a defaults-path unit test (TestViewAt): default_tool/dir_tool surface via ViewAt; an explicit views: entry overrides default_tool and declares a file shape. go build + go test ./... all green. (Next: ServeView config injection from .zddc.d/, the file→form shape, recognizer retirement, client + ./build.) Co-Authored-By: Claude Opus 4.8 (1M context) --- zddc/cmd/zddc-server/main.go | 8 ++++++ zddc/internal/zddc/lookups_test.go | 42 ++++++++++++++++++++++++++++++ zddc/internal/zddc/walker.go | 13 +++++++++ 3 files changed, 63 insertions(+) diff --git a/zddc/cmd/zddc-server/main.go b/zddc/cmd/zddc-server/main.go index f0b977b..dc83314 100644 --- a/zddc/cmd/zddc-server/main.go +++ b/zddc/cmd/zddc-server/main.go @@ -623,6 +623,14 @@ func embeddedVersionsForLog(embedded map[string]string) string { // authenticated user (may be empty). func serveSpecializedNoSlash(cfg config.Config, appsSrv *apps.Server, w http.ResponseWriter, r *http.Request, dirAbs, urlPath, email string) bool { app := apps.DefaultAppAt(cfg.Root, dirAbs) + // An explicit `views.dir` in the cascade overrides the default_tool- + // derived app for the no-slash directory URL — the generalization's + // dir-shape routing. default_tool remains the sugar fallback (ViewAt + // returns it when no views.dir is declared), so existing deployments + // are unaffected. + if v, ok := zddc.ViewAt(cfg.Root, dirAbs, "dir"); ok && v.Tool != "" { + app = v.Tool + } if app == "" { return false } diff --git a/zddc/internal/zddc/lookups_test.go b/zddc/internal/zddc/lookups_test.go index 35618fb..7bdafee 100644 --- a/zddc/internal/zddc/lookups_test.go +++ b/zddc/internal/zddc/lookups_test.go @@ -45,6 +45,48 @@ func TestDefaultToolAt_FromEmbeddedConvention(t *testing.T) { } } +// TestViewAt — default_tool/dir_tool act as sugar for the dir/dir_slash +// shapes, and an explicit views: entry overrides them. +func TestViewAt(t *testing.T) { + resetCache() + root := t.TempDir() + j := func(p ...string) string { return filepath.Join(append([]string{root, "Project-X"}, p...)...) } + + // Sugar: the embedded default_tool/dir_tool surface via ViewAt. + if v, ok := ViewAt(root, j("mdl", "Acme"), "dir"); !ok || v.Tool != "tables" { + t.Errorf("ViewAt(mdl/Acme, dir) = (%+v,%v), want tables", v, ok) + } + if v, ok := ViewAt(root, j("working", "Acme"), "dir"); !ok || v.Tool != "browse" { + t.Errorf("ViewAt(working/Acme, dir) = (%+v,%v), want browse", v, ok) + } + // No file-shape declared by defaults. + if _, ok := ViewAt(root, j("working", "Acme"), "file"); ok { + t.Errorf("ViewAt(working/Acme, file) should be unset by default") + } + + // Explicit views: overrides default_tool and declares a file shape. + resetCache() + dir := j("custom") + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + if err := WriteFile(dir, ZddcFile{ + DefaultTool: "browse", + Views: map[string]ViewSpec{ + "dir": {Tool: "tables", Config: "table.yaml"}, + "file": {Tool: "form", Config: "form.yaml"}, + }, + }); err != nil { + t.Fatal(err) + } + if v, ok := ViewAt(root, dir, "dir"); !ok || v.Tool != "tables" || v.Config != "table.yaml" { + t.Errorf("ViewAt(custom, dir) = (%+v,%v), want {tables,table.yaml}", v, ok) + } + if v, ok := ViewAt(root, dir, "file"); !ok || v.Tool != "form" || v.Config != "form.yaml" { + t.Errorf("ViewAt(custom, file) = (%+v,%v), want {form,form.yaml}", v, ok) + } +} + // TestHistoryAt_Defaults — edit-history defaults on for the live-editing // peers working/mdl/rsk and the ssr registry (subtree-inheriting). The // other peers and the WORM archive do not get history. diff --git a/zddc/internal/zddc/walker.go b/zddc/internal/zddc/walker.go index da106df..0d8d250 100644 --- a/zddc/internal/zddc/walker.go +++ b/zddc/internal/zddc/walker.go @@ -124,6 +124,19 @@ func mergeOverlay(base, top ZddcFile) ZddcFile { out.Tables = mergeStringMap(out.Tables, top.Tables) out.Display = mergeStringMap(out.Display, top.Display) + // Views: per-shape latest-wins (a deeper level overrides a shape, others + // inherit). Mirror of the Roles/Records map merge. + if len(top.Views) > 0 { + merged := make(map[string]ViewSpec, len(out.Views)+len(top.Views)) + for k, v := range out.Views { + merged[k] = v + } + for k, v := range top.Views { + merged[k] = v + } + out.Views = merged + } + // Convert: per-key latest-wins. Pointer-to-struct so we can tell // "absent" from "explicitly empty" — the latter is rare but valid // (an operator who wants to suppress a deployment-default value).