feat(server): route no-slash directory URLs through views.dir (cascade spine)
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) <noreply@anthropic.com>
This commit is contained in:
parent
760cba96c4
commit
45af24b2b1
3 changed files with 63 additions and 0 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
Loading…
Reference in a new issue