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).
|
// authenticated user (may be empty).
|
||||||
func serveSpecializedNoSlash(cfg config.Config, appsSrv *apps.Server, w http.ResponseWriter, r *http.Request, dirAbs, urlPath, email string) bool {
|
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)
|
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 == "" {
|
if app == "" {
|
||||||
return false
|
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
|
// TestHistoryAt_Defaults — edit-history defaults on for the live-editing
|
||||||
// peers working/mdl/rsk and the ssr registry (subtree-inheriting). The
|
// peers working/mdl/rsk and the ssr registry (subtree-inheriting). The
|
||||||
// other peers and the WORM archive do not get history.
|
// 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.Tables = mergeStringMap(out.Tables, top.Tables)
|
||||||
out.Display = mergeStringMap(out.Display, top.Display)
|
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
|
// Convert: per-key latest-wins. Pointer-to-struct so we can tell
|
||||||
// "absent" from "explicitly empty" — the latter is rare but valid
|
// "absent" from "explicitly empty" — the latter is rare but valid
|
||||||
// (an operator who wants to suppress a deployment-default value).
|
// (an operator who wants to suppress a deployment-default value).
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue