feat(zddc): ResolveVirtualView resolver for project-level table aggregations

Models virtualreceived.go's request-time path-rewrite pattern. Recognizes
/<project>/{ssr,mdl,rsk}/... URLs and maps row reads/writes back to
canonical files inside <project>/archive/<party>/, so ACL evaluates
against the per-party chain and operator overrides live where the data
does. ListSSRParties and ListRollupRows feed listing-time synthesis.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-05-18 21:47:28 -05:00
parent da4754b6ef
commit 351dc63cb4
2 changed files with 649 additions and 0 deletions

View file

@ -0,0 +1,361 @@
package zddc
import (
"errors"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)
// Virtual project-level table views — SSR, MDL rollup, RSK rollup.
//
// All three are declared `virtual: true` in defaults.zddc.yaml under
// `<project>/{ssr,mdl,rsk}`. The folder does not exist on disk: the
// server synthesizes listings by walking archive/*/ at request time
// and rewrites file reads/writes back to canonical paths inside the
// per-party folders. ACL on each synthetic row is evaluated against
// the canonical `<project>/archive/<party>/` chain, so party owners
// can edit their own rows and non-owners see them read-only.
//
// URL conventions
//
// /<project>/ssr/ → directory listing
// /<project>/ssr/table.yaml | form.yaml → embedded default spec
// /<project>/ssr/<party>.yaml → reads <project>/archive/<party>/ssr.yaml
// /<project>/ssr/<party>.yaml.html → form edit (form recognizer)
// /<project>/ssr/form.html → "+ Add row" — SSR create
//
// /<project>/mdl/ → rollup listing
// /<project>/mdl/table.yaml | form.yaml → embedded default project-rollup spec
// /<project>/mdl/<party>__<file>.yaml → reads <project>/archive/<party>/mdl/<file>.yaml
// /<project>/mdl/<party>__<file>.yaml.html → form edit
//
// /<project>/rsk/ → analogous
//
// Modeled on virtualreceived.go: one resolver produces canonical
// paths; every caller (listing builder, file API rewrite, form
// recognizer) reads its policy chain from the canonical path.
// VirtualViewKind classifies a resolved virtual URL.
type VirtualViewKind int
const (
VirtualViewNone VirtualViewKind = iota
VirtualViewSSRRoot
VirtualViewSSRSpec
VirtualViewSSRRow
VirtualViewMDLRoot
VirtualViewMDLSpec
VirtualViewMDLRow
VirtualViewRSKRoot
VirtualViewRSKSpec
VirtualViewRSKRow
)
// IsRowKind reports whether k targets a per-party row file (true for
// SSRRow, MDLRow, RSKRow).
func (k VirtualViewKind) IsRowKind() bool {
switch k {
case VirtualViewSSRRow, VirtualViewMDLRow, VirtualViewRSKRow:
return true
}
return false
}
// IsSpecKind reports whether k targets a virtual table.yaml/form.yaml.
func (k VirtualViewKind) IsSpecKind() bool {
switch k {
case VirtualViewSSRSpec, VirtualViewMDLSpec, VirtualViewRSKSpec:
return true
}
return false
}
// IsRootKind reports whether k targets the listing-level URL of a
// virtual view.
func (k VirtualViewKind) IsRootKind() bool {
switch k {
case VirtualViewSSRRoot, VirtualViewMDLRoot, VirtualViewRSKRoot:
return true
}
return false
}
// VirtualViewResolution captures the result of mapping a URL onto
// one of the project-level virtual table views. All fields are
// populated only when Resolved is true.
type VirtualViewResolution struct {
Resolved bool
Kind VirtualViewKind
Project string // "<project>"
ProjectURL string // "/<project>/"
ProjectAbs string // <fsRoot>/<project>
Slot string // "ssr", "mdl", or "rsk"
SlotURL string // "/<project>/<slot>/"
// Populated for VirtualView*Spec kinds: "table.yaml" or "form.yaml".
SpecBase string
// Populated for VirtualView*Row kinds.
Party string // party folder name (e.g. "0330C1")
PartyArchive string // <fsRoot>/<project>/archive/<party>
CanonicalAbs string // underlying file on disk
CanonicalURL string // /<project>/archive/<party>/...
SchemaAbs string // SSR only — <party>/ssr.form.yaml (may not exist; falls back to embedded)
RowFilename string // MDL/RSK rollups only — e.g. "D-001.yaml"
}
// virtualViewRE matches /<project>/<slot>[/<rest>] where slot is one
// of the canonical virtual view names. Capture 1 = project, capture
// 2 = slot, capture 3 = rest (may be empty).
var virtualViewRE = regexp.MustCompile(`^/([^/]+)/(ssr|mdl|rsk)(?:/(.*))?$`)
// partyNameRE matches the SSR schema's `name` pattern. Same regex
// used at row-resolution time so URLs with invalid party tokens fail
// resolution cleanly instead of producing impossible canonical paths.
var partyNameRE = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9.-]*$`)
// ValidPartyName reports whether s is a valid party folder name —
// starts with [A-Za-z0-9], then any of [A-Za-z0-9.-]. Used by URL
// resolution AND by the SSR create handler to validate user input.
func ValidPartyName(s string) bool {
return partyNameRE.MatchString(s)
}
// ResolveVirtualView inspects urlPath and returns a populated
// resolution iff the URL targets one of the project-level virtual
// views (ssr/, mdl/, rsk/). On a non-match, Resolved=false.
//
// The resolver does NOT check that the project / party / row file
// actually exist on disk — that's the caller's job (handlers use
// the canonical path; listings synthesize from real disk state).
//
// urlPath must be a server-relative URL with one leading slash.
// Trailing slashes are tolerated for root kinds.
func ResolveVirtualView(fsRoot, urlPath string) VirtualViewResolution {
var out VirtualViewResolution
if urlPath == "" || urlPath[0] != '/' {
return out
}
trimmed := strings.TrimSuffix(urlPath, "/")
m := virtualViewRE.FindStringSubmatch(trimmed)
if m == nil {
return out
}
project := m[1]
slot := m[2]
rest := m[3]
if project == "" || strings.HasPrefix(project, ".") || strings.HasPrefix(project, "_") {
return out
}
projectAbs := filepath.Join(fsRoot, filepath.FromSlash(project))
if !strings.HasPrefix(projectAbs, fsRoot+string(filepath.Separator)) && projectAbs != fsRoot {
return out
}
out.Project = project
out.ProjectURL = "/" + project + "/"
out.ProjectAbs = projectAbs
out.Slot = slot
out.SlotURL = "/" + project + "/" + slot + "/"
if rest == "" {
switch slot {
case "ssr":
out.Kind = VirtualViewSSRRoot
case "mdl":
out.Kind = VirtualViewMDLRoot
case "rsk":
out.Kind = VirtualViewRSKRoot
}
out.Resolved = true
return out
}
if rest == "table.yaml" || rest == "form.yaml" {
switch slot {
case "ssr":
out.Kind = VirtualViewSSRSpec
case "mdl":
out.Kind = VirtualViewMDLSpec
case "rsk":
out.Kind = VirtualViewRSKSpec
}
out.SpecBase = rest
out.Resolved = true
return out
}
// Row files — must be a single segment ending in .yaml.
if strings.Contains(rest, "/") || !strings.HasSuffix(rest, ".yaml") {
return out
}
name := strings.TrimSuffix(rest, ".yaml")
if slot == "ssr" {
if !ValidPartyName(name) {
return out
}
out.Party = name
out.PartyArchive = filepath.Join(projectAbs, "archive", name)
out.CanonicalAbs = filepath.Join(out.PartyArchive, "ssr.yaml")
out.CanonicalURL = "/" + project + "/archive/" + name + "/ssr.yaml"
out.SchemaAbs = filepath.Join(out.PartyArchive, "ssr.form.yaml")
out.Kind = VirtualViewSSRRow
out.Resolved = true
return out
}
// MDL/RSK rollup row — <party>__<file>.yaml.
idx := strings.Index(name, "__")
if idx <= 0 || idx >= len(name)-2 {
return out
}
party := name[:idx]
rowBase := name[idx+2:]
if !ValidPartyName(party) || rowBase == "" || strings.Contains(rowBase, "__") {
return out
}
out.Party = party
out.PartyArchive = filepath.Join(projectAbs, "archive", party)
out.RowFilename = rowBase + ".yaml"
out.CanonicalAbs = filepath.Join(out.PartyArchive, slot, out.RowFilename)
out.CanonicalURL = "/" + project + "/archive/" + party + "/" + slot + "/" + out.RowFilename
switch slot {
case "mdl":
out.Kind = VirtualViewMDLRow
case "rsk":
out.Kind = VirtualViewRSKRow
}
out.Resolved = true
return out
}
// IsSSRCreateURL reports whether urlPath is /<project>/ssr/form.html
// — the SSR "+ Add row" target. Returns the project name when matched.
func IsSSRCreateURL(urlPath string) (string, bool) {
if urlPath == "" || urlPath[0] != '/' {
return "", false
}
parts := strings.Split(strings.TrimPrefix(urlPath, "/"), "/")
if len(parts) != 3 || parts[1] != "ssr" || parts[2] != "form.html" {
return "", false
}
project := parts[0]
if project == "" || strings.HasPrefix(project, ".") || strings.HasPrefix(project, "_") {
return "", false
}
return project, true
}
// StripYAMLHTML returns urlPath with a trailing ".html" stripped iff
// the URL has the form-edit shape ".../<name>.yaml.html". Otherwise
// returns urlPath unchanged + false. The form recognizer calls this
// before passing the data URL into ResolveVirtualView.
func StripYAMLHTML(urlPath string) (string, bool) {
if strings.HasSuffix(urlPath, ".yaml.html") {
return strings.TrimSuffix(urlPath, ".html"), true
}
return urlPath, false
}
// ListSSRParties returns the party folder names that exist under
// <project>/archive/. Names are filtered through ValidPartyName so a
// hand-created folder with a weird name (e.g. "0330C1 (draft)") won't
// confuse the rest of the resolver. Returns nil + nil when archive/
// doesn't exist on disk.
func ListSSRParties(fsRoot, projectAbs string) ([]string, error) {
archive := filepath.Join(projectAbs, "archive")
entries, err := os.ReadDir(archive)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
return nil, err
}
out := make([]string, 0, len(entries))
for _, e := range entries {
if !e.IsDir() {
continue
}
name := e.Name()
if !ValidPartyName(name) {
continue
}
out = append(out, name)
}
sort.Strings(out)
return out, nil
}
// VirtualRollupRow describes one synthetic row in a project-level
// MDL or RSK rollup.
type VirtualRollupRow struct {
Party string // source party folder
Filename string // e.g. "D-001.yaml"
SyntheticName string // e.g. "0330C1__D-001.yaml" — used in URLs
CanonicalAbs string // underlying file on disk
}
// ListRollupRows walks <project>/archive/*/<slot>/ and returns one
// synthetic row per *.yaml file. slot must be "mdl" or "rsk".
// Returns rows sorted by (party, filename).
//
// Skipped:
// - filenames containing "__" (would break the party__file split)
// - "table.yaml" and "form.yaml" (operator spec/schema, not rows)
// - any non-*.yaml file
// - parties with invalid folder names (filtered by ListSSRParties)
func ListRollupRows(fsRoot, projectAbs, slot string) ([]VirtualRollupRow, error) {
if slot != "mdl" && slot != "rsk" {
return nil, errors.New("ListRollupRows: slot must be mdl or rsk")
}
parties, err := ListSSRParties(fsRoot, projectAbs)
if err != nil {
return nil, err
}
out := make([]VirtualRollupRow, 0, len(parties))
for _, party := range parties {
slotDir := filepath.Join(projectAbs, "archive", party, slot)
entries, err := os.ReadDir(slotDir)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
continue
}
return nil, err
}
for _, e := range entries {
if e.IsDir() {
continue
}
name := e.Name()
if !strings.HasSuffix(name, ".yaml") {
continue
}
if name == "table.yaml" || name == "form.yaml" {
continue
}
if strings.Contains(name, "__") {
continue
}
out = append(out, VirtualRollupRow{
Party: party,
Filename: name,
SyntheticName: party + "__" + name,
CanonicalAbs: filepath.Join(slotDir, name),
})
}
}
sort.Slice(out, func(i, j int) bool {
if out[i].Party != out[j].Party {
return out[i].Party < out[j].Party
}
return out[i].Filename < out[j].Filename
})
return out, nil
}

View file

@ -0,0 +1,288 @@
package zddc
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestResolveVirtualView_Roots(t *testing.T) {
root := t.TempDir()
cases := []struct {
url string
want VirtualViewKind
}{
{"/Project/ssr", VirtualViewSSRRoot},
{"/Project/ssr/", VirtualViewSSRRoot},
{"/Project/mdl", VirtualViewMDLRoot},
{"/Project/mdl/", VirtualViewMDLRoot},
{"/Project/rsk", VirtualViewRSKRoot},
{"/Project/rsk/", VirtualViewRSKRoot},
}
for _, tc := range cases {
got := ResolveVirtualView(root, tc.url)
if !got.Resolved || got.Kind != tc.want {
t.Errorf("%s: kind=%d resolved=%v; want kind=%d resolved=true", tc.url, got.Kind, got.Resolved, tc.want)
}
if got.Project != "Project" {
t.Errorf("%s: project=%q want Project", tc.url, got.Project)
}
if !got.Kind.IsRootKind() {
t.Errorf("%s: IsRootKind=false", tc.url)
}
}
}
func TestResolveVirtualView_Specs(t *testing.T) {
root := t.TempDir()
cases := []struct {
url string
wantKind VirtualViewKind
wantBase string
}{
{"/Project/ssr/table.yaml", VirtualViewSSRSpec, "table.yaml"},
{"/Project/ssr/form.yaml", VirtualViewSSRSpec, "form.yaml"},
{"/Project/mdl/table.yaml", VirtualViewMDLSpec, "table.yaml"},
{"/Project/mdl/form.yaml", VirtualViewMDLSpec, "form.yaml"},
{"/Project/rsk/table.yaml", VirtualViewRSKSpec, "table.yaml"},
{"/Project/rsk/form.yaml", VirtualViewRSKSpec, "form.yaml"},
}
for _, tc := range cases {
got := ResolveVirtualView(root, tc.url)
if !got.Resolved || got.Kind != tc.wantKind {
t.Errorf("%s: kind=%d resolved=%v; want kind=%d", tc.url, got.Kind, got.Resolved, tc.wantKind)
}
if got.SpecBase != tc.wantBase {
t.Errorf("%s: SpecBase=%q want %q", tc.url, got.SpecBase, tc.wantBase)
}
if !got.Kind.IsSpecKind() {
t.Errorf("%s: IsSpecKind=false", tc.url)
}
}
}
func TestResolveVirtualView_SSRRow(t *testing.T) {
root := t.TempDir()
got := ResolveVirtualView(root, "/Project/ssr/0330C1.yaml")
if !got.Resolved || got.Kind != VirtualViewSSRRow {
t.Fatalf("unexpected resolution: %+v", got)
}
if got.Party != "0330C1" {
t.Errorf("Party=%q want 0330C1", got.Party)
}
wantAbs := filepath.Join(root, "Project", "archive", "0330C1", "ssr.yaml")
if got.CanonicalAbs != wantAbs {
t.Errorf("CanonicalAbs=%q want %q", got.CanonicalAbs, wantAbs)
}
wantSchema := filepath.Join(root, "Project", "archive", "0330C1", "ssr.form.yaml")
if got.SchemaAbs != wantSchema {
t.Errorf("SchemaAbs=%q want %q", got.SchemaAbs, wantSchema)
}
if !got.Kind.IsRowKind() {
t.Errorf("IsRowKind=false")
}
}
func TestResolveVirtualView_RollupRow(t *testing.T) {
root := t.TempDir()
cases := []struct {
url string
wantKind VirtualViewKind
wantParty string
wantFilename string
wantSlot string
}{
{"/Project/mdl/0330C1__D-001.yaml", VirtualViewMDLRow, "0330C1", "D-001.yaml", "mdl"},
{"/Project/rsk/Acme__R-005.yaml", VirtualViewRSKRow, "Acme", "R-005.yaml", "rsk"},
}
for _, tc := range cases {
got := ResolveVirtualView(root, tc.url)
if !got.Resolved || got.Kind != tc.wantKind {
t.Errorf("%s: kind=%d resolved=%v; want kind=%d", tc.url, got.Kind, got.Resolved, tc.wantKind)
continue
}
if got.Party != tc.wantParty {
t.Errorf("%s: Party=%q want %q", tc.url, got.Party, tc.wantParty)
}
if got.RowFilename != tc.wantFilename {
t.Errorf("%s: RowFilename=%q want %q", tc.url, got.RowFilename, tc.wantFilename)
}
wantAbs := filepath.Join(root, "Project", "archive", tc.wantParty, tc.wantSlot, tc.wantFilename)
if got.CanonicalAbs != wantAbs {
t.Errorf("%s: CanonicalAbs=%q want %q", tc.url, got.CanonicalAbs, wantAbs)
}
}
}
func TestResolveVirtualView_NonMatches(t *testing.T) {
root := t.TempDir()
cases := []string{
"/",
"/Project",
"/Project/",
"/Project/working",
"/Project/archive/Acme/mdl",
"/Project/ssr/invalid__name__double.yaml", // double-double underscore is rejected
"/Project/mdl/__leading.yaml", // empty party
"/Project/mdl/party__.yaml", // empty rowBase
"/Project/ssr/.hidden.yaml", // dotfile party name
"/Project/ssr/0330C1.yaml/sub", // sub-path under row file
"/Project/notaslot/table.yaml",
}
for _, url := range cases {
got := ResolveVirtualView(root, url)
if got.Resolved {
t.Errorf("%s: unexpectedly resolved as kind %d", url, got.Kind)
}
}
}
func TestIsSSRCreateURL(t *testing.T) {
cases := []struct {
url string
want string
wantOK bool
}{
{"/Project/ssr/form.html", "Project", true},
{"/Other-Project/ssr/form.html", "Other-Project", true},
{"/Project/ssr/", "", false},
{"/Project/ssr/Acme.yaml.html", "", false},
{"/Project/mdl/form.html", "", false},
{"/.hidden/ssr/form.html", "", false},
}
for _, tc := range cases {
got, ok := IsSSRCreateURL(tc.url)
if ok != tc.wantOK {
t.Errorf("%s: ok=%v want %v", tc.url, ok, tc.wantOK)
}
if got != tc.want {
t.Errorf("%s: project=%q want %q", tc.url, got, tc.want)
}
}
}
func TestStripYAMLHTML(t *testing.T) {
cases := []struct {
in string
want string
wantOK bool
}{
{"/Project/ssr/Acme.yaml.html", "/Project/ssr/Acme.yaml", true},
{"/Project/mdl/foo__bar.yaml.html", "/Project/mdl/foo__bar.yaml", true},
{"/Project/ssr/Acme.yaml", "/Project/ssr/Acme.yaml", false},
{"/Project/ssr/form.html", "/Project/ssr/form.html", false},
}
for _, tc := range cases {
got, ok := StripYAMLHTML(tc.in)
if got != tc.want || ok != tc.wantOK {
t.Errorf("%s: (%q, %v) want (%q, %v)", tc.in, got, ok, tc.want, tc.wantOK)
}
}
}
func TestValidPartyName(t *testing.T) {
ok := []string{"0330C1", "Acme", "Acme.Inc", "Acme-Subsidiary", "a", "0", "0330C1.draft", "X-Y-Z"}
bad := []string{"", ".hidden", "_underscore", "Acme/sub", "Acme Inc", "Acme(Inc)", "Acme,Inc", "..", "-leading-dash"}
for _, s := range ok {
if !ValidPartyName(s) {
t.Errorf("ValidPartyName(%q) = false, want true", s)
}
}
for _, s := range bad {
if ValidPartyName(s) {
t.Errorf("ValidPartyName(%q) = true, want false", s)
}
}
}
func TestListSSRParties(t *testing.T) {
root := t.TempDir()
projectAbs := filepath.Join(root, "Project")
for _, party := range []string{"0330C1", "0440P2", "Acme"} {
if err := os.MkdirAll(filepath.Join(projectAbs, "archive", party), 0o755); err != nil {
t.Fatal(err)
}
}
// A file (not a dir) and a hidden folder should be filtered out.
if err := os.WriteFile(filepath.Join(projectAbs, "archive", "stray.txt"), []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(projectAbs, "archive", ".hidden"), 0o755); err != nil {
t.Fatal(err)
}
parties, err := ListSSRParties(root, projectAbs)
if err != nil {
t.Fatal(err)
}
want := []string{"0330C1", "0440P2", "Acme"}
if strings.Join(parties, ",") != strings.Join(want, ",") {
t.Errorf("got %v, want %v", parties, want)
}
}
func TestListSSRParties_NoArchive(t *testing.T) {
root := t.TempDir()
projectAbs := filepath.Join(root, "Project")
parties, err := ListSSRParties(root, projectAbs)
if err != nil {
t.Fatalf("err=%v want nil", err)
}
if len(parties) != 0 {
t.Errorf("got %v, want empty", parties)
}
}
func TestListRollupRows(t *testing.T) {
root := t.TempDir()
projectAbs := filepath.Join(root, "Project")
for _, party := range []string{"0330C1", "0440P2"} {
mdlDir := filepath.Join(projectAbs, "archive", party, "mdl")
if err := os.MkdirAll(mdlDir, 0o755); err != nil {
t.Fatal(err)
}
}
// Real rows.
if err := os.WriteFile(filepath.Join(projectAbs, "archive", "0330C1", "mdl", "D-001.yaml"), []byte("id: D-001\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(projectAbs, "archive", "0330C1", "mdl", "D-002.yaml"), []byte("id: D-002\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(projectAbs, "archive", "0440P2", "mdl", "D-010.yaml"), []byte("id: D-010\n"), 0o644); err != nil {
t.Fatal(err)
}
// Skipped: table.yaml, form.yaml, anything containing "__".
if err := os.WriteFile(filepath.Join(projectAbs, "archive", "0330C1", "mdl", "table.yaml"), []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(projectAbs, "archive", "0330C1", "mdl", "form.yaml"), []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(projectAbs, "archive", "0330C1", "mdl", "weird__name.yaml"), []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
rows, err := ListRollupRows(root, projectAbs, "mdl")
if err != nil {
t.Fatal(err)
}
if len(rows) != 3 {
t.Fatalf("got %d rows, want 3; rows=%+v", len(rows), rows)
}
wantNames := []string{"0330C1__D-001.yaml", "0330C1__D-002.yaml", "0440P2__D-010.yaml"}
for i, want := range wantNames {
if rows[i].SyntheticName != want {
t.Errorf("row[%d].SyntheticName=%q want %q", i, rows[i].SyntheticName, want)
}
}
}
func TestListRollupRows_BadSlot(t *testing.T) {
root := t.TempDir()
if _, err := ListRollupRows(root, root, "ssr"); err == nil {
t.Error("expected error for slot=ssr (only mdl/rsk valid)")
}
}