Refactor: migrate POSIX scaffolding to CBOR and JSONC

This commit is contained in:
MangoPig
2026-07-06 13:06:32 +01:00
parent 2c8338d1cd
commit 58bc285748
14 changed files with 860 additions and 268 deletions
@@ -28,8 +28,10 @@ func deriveScope(relPath string, rootScope Scope) Scope {
func classifyPath(relPath string, isDir bool) (logicalType, fileRole string) {
parts := strings.Split(relPath, "/")
name := parts[len(parts)-1]
ext := strings.ToLower(filepath.Ext(name))
structuredRole := strings.TrimSuffix(name, filepath.Ext(name))
if !isDir {
fileRole = strings.TrimSuffix(name, filepath.Ext(name))
fileRole = structuredRole
}
hasChildrenAncestor := pathContainsSegment(parts, "children")
@@ -40,9 +42,9 @@ func classifyPath(relPath string, isDir bool) (logicalType, fileRole string) {
}
switch {
case relPath == "settings.json":
case !isDir && structuredRole == "settings" && len(parts) == 1 && ext == ".cbor":
return "tenant", "settings"
case relPath == "layout.json":
case !isDir && structuredRole == "layout" && len(parts) == 1 && ext == ".cbor":
return "tenant", "layout"
case len(parts) >= 1 && parts[0] == "catalog":
if isDir {
@@ -50,60 +52,110 @@ func classifyPath(relPath string, isDir bool) (logicalType, fileRole string) {
return "catalog", ""
}
if len(parts) >= 2 && parts[1] == "packs" {
if len(parts) == 2 { return "catalog_packs", "" }
if len(parts) == 3 { return "catalog_pack", "" }
if len(parts) >= 4 && parts[3] == "entries" { return "catalog_pack_entries", "" }
if len(parts) == 2 {
return "catalog_packs", ""
}
if len(parts) == 3 {
return "catalog_pack", ""
}
if len(parts) >= 4 && parts[3] == "entries" {
return "catalog_pack_entries", ""
}
return "catalog_entry", ""
}
if len(parts) >= 2 && parts[1] == "standalone" {
if len(parts) == 2 { return "catalog_standalone", "" }
if len(parts) == 2 {
return "catalog_standalone", ""
}
return "catalog_entry", ""
}
}
return "catalog", fileRole
case len(parts) >= 2 && parts[0] == "departments" && strings.HasPrefix(parts[1], "department-"):
if isDir {
if len(parts) == 2 { return "department", "" }
if len(parts) == 3 && parts[2] == "teams" { return "department_teams", "" }
if len(parts) >= 4 && strings.HasPrefix(parts[3], "team-") { return "team", "" }
if len(parts) == 2 {
return "department", ""
}
if len(parts) == 3 && parts[2] == "teams" {
return "department_teams", ""
}
if len(parts) >= 4 && strings.HasPrefix(parts[3], "team-") {
return "team", ""
}
}
if len(parts) >= 4 && strings.HasPrefix(parts[3], "team-") {
return "team", fileRole
}
if len(parts) >= 4 && strings.HasPrefix(parts[3], "team-") { return "team", fileRole }
return "department", fileRole
case len(parts) >= 2 && parts[0] == "projects" && strings.HasPrefix(parts[1], "project-"):
if isDir {
if strings.HasPrefix(name, "project-") { return "project", "" }
if name == "children" { return "project_children", "" }
if name == "tree" { return "project_tree", "" }
if hasChildrenAncestor && strings.HasPrefix(name, "folder-") { return "hierarchy_folder", "" }
if hasTreeAncestor && strings.HasPrefix(name, "folder-") { return "hierarchy_folder", "" }
if hasTreeAncestor && strings.HasPrefix(name, "item-") { return "item", "" }
if strings.HasPrefix(name, "project-") {
return "project", ""
}
if name == "children" {
return "project_children", ""
}
if name == "tree" {
return "project_tree", ""
}
if hasChildrenAncestor && strings.HasPrefix(name, "folder-") {
return "hierarchy_folder", ""
}
if hasTreeAncestor && strings.HasPrefix(name, "folder-") {
return "hierarchy_folder", ""
}
if hasTreeAncestor && strings.HasPrefix(name, "item-") {
return "item", ""
}
}
if hasChildrenAncestor && strings.HasPrefix(parentName, "folder-") {
return "hierarchy_folder", fileRole
}
if hasChildrenAncestor && strings.HasPrefix(parentName, "folder-") { return "hierarchy_folder", fileRole }
if hasTreeAncestor {
if strings.HasPrefix(parentName, "item-") { return "item", fileRole }
if strings.HasPrefix(parentName, "folder-") { return "hierarchy_folder", fileRole }
if strings.HasPrefix(parentName, "item-") {
return "item", fileRole
}
if strings.HasPrefix(parentName, "folder-") {
return "hierarchy_folder", fileRole
}
}
return "project", fileRole
case len(parts) >= 1 && parts[0] == "users":
if isDir {
if len(parts) == 1 { return "users", "" }
if len(parts) == 2 && parts[1] == "personals" { return "personals", "" }
if len(parts) >= 3 && parts[1] == "personals" && strings.HasPrefix(parts[2], "personal-") { return "personal", "" }
if len(parts) == 1 {
return "users", ""
}
if len(parts) == 2 && parts[1] == "personals" {
return "personals", ""
}
if len(parts) >= 3 && parts[1] == "personals" && strings.HasPrefix(parts[2], "personal-") {
return "personal", ""
}
if strings.Contains(relPath, "/tree/") || strings.HasSuffix(relPath, "/tree") {
if strings.HasPrefix(name, "folder-") { return "folder", "" }
if strings.HasPrefix(name, "item-") { return "item", "" }
if strings.HasPrefix(name, "folder-") {
return "folder", ""
}
if strings.HasPrefix(name, "item-") {
return "item", ""
}
}
}
if len(parts) >= 3 && parts[1] == "personals" && strings.HasPrefix(parts[2], "personal-") {
if strings.Contains(relPath, "/tree/") {
if strings.HasPrefix(parts[len(parts)-2], "item-") { return "item", fileRole }
if strings.HasPrefix(parts[len(parts)-2], "folder-") { return "folder", fileRole }
if strings.HasPrefix(parts[len(parts)-2], "item-") {
return "item", fileRole
}
if strings.HasPrefix(parts[len(parts)-2], "folder-") {
return "folder", fileRole
}
}
return "personal", fileRole
}
return "users", fileRole
default:
if isDir { return "directory", "" }
if isDir {
return "directory", ""
}
return "file", fileRole
}
}
+96 -8
View File
@@ -11,6 +11,13 @@ import (
"os"
"path/filepath"
"strings"
"github.com/fxamacker/cbor/v2"
"github.com/tailscale/hujson"
)
const (
settingsCBORPath = "settings.cbor"
)
func ScanRoot(root string) ([]Node, error) {
@@ -77,18 +84,21 @@ func ScanRoot(root string) ([]Node, error) {
}
func loadRootScope(rootPath string) (Scope, error) {
settingsPath := filepath.Join(rootPath, "settings.json")
settingsPath := filepath.Join(rootPath, settingsCBORPath)
content, err := os.ReadFile(settingsPath)
if err != nil {
if errorsIsNotExist(err) {
return Scope{}, nil
}
return Scope{}, fmt.Errorf("read root settings.json: %w", err)
return Scope{}, fmt.Errorf("read root %s: %w", filepath.Base(settingsPath), err)
}
var payload map[string]any
if err := json.Unmarshal(content, &payload); err != nil {
return Scope{}, fmt.Errorf("decode root settings.json: %w", err)
payload, err := decodeStructuredMap(settingsPath, content)
if err != nil {
return Scope{}, fmt.Errorf("decode root %s: %w", filepath.Base(settingsPath), err)
}
if len(payload) == 0 {
return Scope{}, nil
}
installation, _ := payload["installation"].(map[string]any)
@@ -138,9 +148,9 @@ func buildNode(rootPath, relPath string, entry fs.DirEntry, rootScope Scope) (No
node.SizeBytes = int64(len(content))
node.Checksum = hex.EncodeToString(hash[:])
if strings.EqualFold(filepath.Ext(entry.Name()), ".json") {
var payload map[string]any
if err := json.Unmarshal(content, &payload); err == nil {
if isStructuredProjectionFile(entry.Name()) {
payload, err := decodeStructuredMap(absPath, content)
if err == nil {
jsonContent, err := json.Marshal(payload)
if err != nil {
return Node{}, fmt.Errorf("remarshal POSIX file %s: %w", relPath, err)
@@ -171,6 +181,84 @@ func buildNode(rootPath, relPath string, entry fs.DirEntry, rootScope Scope) (No
return node, nil
}
func isStructuredProjectionFile(name string) bool {
switch strings.ToLower(filepath.Ext(name)) {
case ".json", ".jsonc", ".cbor":
return true
default:
return false
}
}
func decodeStructuredMap(path string, content []byte) (map[string]any, error) {
var payload any
switch strings.ToLower(filepath.Ext(path)) {
case ".cbor":
if err := cbor.Unmarshal(content, &payload); err != nil {
return nil, err
}
case ".json":
if err := json.Unmarshal(content, &payload); err != nil {
return nil, err
}
case ".jsonc":
decoded, err := decodeJSONCToAny(content)
if err != nil {
return nil, err
}
payload = decoded
default:
return nil, fmt.Errorf("unsupported structured file extension %q", filepath.Ext(path))
}
if payload == nil {
return map[string]any{}, nil
}
normalized, ok := normalizeStructuredValue(payload).(map[string]any)
if !ok || normalized == nil {
return map[string]any{}, nil
}
return normalized, nil
}
func decodeJSONCToAny(content []byte) (any, error) {
ast, err := hujson.Parse(content)
if err != nil {
return nil, err
}
ast.Standardize()
standardized := ast.Pack()
var payload any
if err := json.Unmarshal(standardized, &payload); err != nil {
return nil, err
}
return payload, nil
}
func normalizeStructuredValue(value any) any {
switch typed := value.(type) {
case map[string]any:
normalized := make(map[string]any, len(typed))
for key, child := range typed {
normalized[key] = normalizeStructuredValue(child)
}
return normalized
case map[any]any:
normalized := make(map[string]any, len(typed))
for key, child := range typed {
normalized[fmt.Sprint(key)] = normalizeStructuredValue(child)
}
return normalized
case []any:
normalized := make([]any, len(typed))
for index, child := range typed {
normalized[index] = normalizeStructuredValue(child)
}
return normalized
default:
return value
}
}
func summarizeNodes(nodes []Node) RebuildSummary {
summary := RebuildSummary{TotalNodes: len(nodes)}
for _, node := range nodes {
+116 -34
View File
@@ -7,6 +7,21 @@ import (
"os"
"path/filepath"
"testing"
"github.com/fxamacker/cbor/v2"
)
const (
testSettingsFileName = "settings.cbor"
testLayoutFileName = "layout.cbor"
testHomeFileName = "home.cbor"
testUsersFileName = "users.cbor"
testACLFileName = "acl.cbor"
testFolderFileName = "folder.cbor"
testItemFileName = "item.cbor"
testDataFileName = "data.cbor"
testSchemaFileName = "schema.json"
testManifestFileName = "manifest.jsonc"
)
func TestScanRootBuildsProjectedNodesFromBootstrapShape(t *testing.T) {
@@ -14,6 +29,8 @@ func TestScanRootBuildsProjectedNodesFromBootstrapShape(t *testing.T) {
mustMkdirAll(t, filepath.Join(root, "catalog", "packs"))
mustMkdirAll(t, filepath.Join(root, "catalog", "standalone"))
mustMkdirAll(t, filepath.Join(root, "catalog", "packs", "pack-core", "entries", "app-shell"))
mustMkdirAll(t, filepath.Join(root, "catalog", "standalone", "app-shell"))
mustMkdirAll(t, filepath.Join(root, "departments", "department-primary-department", "teams", "team-primary-team"))
mustMkdirAll(t, filepath.Join(root, "projects", "project-primary-project", "children", "folder-design", "children", "project-web", "children"))
mustMkdirAll(t, filepath.Join(root, "projects", "project-primary-project", "children", "folder-design", "children", "project-web", "tree"))
@@ -22,7 +39,7 @@ func TestScanRootBuildsProjectedNodesFromBootstrapShape(t *testing.T) {
mustMkdirAll(t, filepath.Join(root, "projects", "project-primary-project", "tree", "folder-docs", "item-roadmap"))
mustMkdirAll(t, filepath.Join(root, "users", "personals"))
mustWriteJSON(t, filepath.Join(root, "settings.json"), map[string]any{
mustWriteStructured(t, filepath.Join(root, testSettingsFileName), map[string]any{
"installation": map[string]any{
"id": "installation-1",
"name": "MangoPig",
@@ -34,81 +51,103 @@ func TestScanRootBuildsProjectedNodesFromBootstrapShape(t *testing.T) {
"slug": "primary-organization",
},
})
mustWriteJSON(t, filepath.Join(root, "layout.json"), map[string]any{
mustWriteStructured(t, filepath.Join(root, testLayoutFileName), map[string]any{
"type": "tenant-layout",
"home": map[string]any{"defaultProjectSlug": "primary-project"},
})
mustWriteJSON(t, filepath.Join(root, "departments", "department-primary-department", "settings.json"), map[string]any{
mustWriteStructured(t, filepath.Join(root, "catalog", "packs", "pack-core", testManifestFileName), map[string]any{
"id": "pack-core",
"slug": "core",
"type": "pack",
"name": "Core Pack",
"entries": []map[string]any{{"slug": "app-shell", "path": "entries/app-shell"}},
})
mustWriteStructured(t, filepath.Join(root, "catalog", "packs", "pack-core", "entries", "app-shell", testManifestFileName), map[string]any{
"id": "app-shell",
"slug": "app-shell",
"type": "app",
"runtime": map[string]any{
"kind": "route",
"path": "/v1/app-shell",
},
})
mustWriteStructured(t, filepath.Join(root, "catalog", "standalone", "app-shell", testManifestFileName), map[string]any{
"id": "app-shell",
"slug": "app-shell",
"type": "app",
"source": "standalone",
})
mustWriteStructured(t, filepath.Join(root, "departments", "department-primary-department", testSettingsFileName), map[string]any{
"id": "dept-1",
"name": "Primary Department",
"slug": "primary-department",
"type": "department",
})
mustWriteJSON(t, filepath.Join(root, "departments", "department-primary-department", "users.json"), map[string]any{
mustWriteStructured(t, filepath.Join(root, "departments", "department-primary-department", testUsersFileName), map[string]any{
"users": []map[string]any{{"id": "admin-1"}},
})
mustWriteJSON(t, filepath.Join(root, "departments", "department-primary-department", "teams", "team-primary-team", "settings.json"), map[string]any{
mustWriteStructured(t, filepath.Join(root, "departments", "department-primary-department", "teams", "team-primary-team", testSettingsFileName), map[string]any{
"id": "team-1",
"name": "Primary Team",
"slug": "primary-team",
"type": "team",
})
mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "settings.json"), map[string]any{
mustWriteStructured(t, filepath.Join(root, "projects", "project-primary-project", testSettingsFileName), map[string]any{
"id": "project-1",
"name": "Primary Project",
"slug": "primary-project",
"type": "project",
})
mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "home.json"), map[string]any{
mustWriteStructured(t, filepath.Join(root, "projects", "project-primary-project", testHomeFileName), map[string]any{
"type": "project-home",
"project": "primary-project",
})
mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "acl.json"), map[string]any{
mustWriteStructured(t, filepath.Join(root, "projects", "project-primary-project", testACLFileName), map[string]any{
"inherits": true,
"rules": []any{},
})
mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "children", "folder-design", "folder.json"), map[string]any{
mustWriteStructured(t, filepath.Join(root, "projects", "project-primary-project", "children", "folder-design", testFolderFileName), map[string]any{
"name": "Design",
"slug": "design",
})
mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "children", "folder-design", "acl.json"), map[string]any{
mustWriteStructured(t, filepath.Join(root, "projects", "project-primary-project", "children", "folder-design", testACLFileName), map[string]any{
"inherits": true,
"rules": []any{},
})
mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "children", "folder-design", "children", "project-web", "settings.json"), map[string]any{
mustWriteStructured(t, filepath.Join(root, "projects", "project-primary-project", "children", "folder-design", "children", "project-web", testSettingsFileName), map[string]any{
"id": "project-2",
"name": "Web Project",
"slug": "web",
"type": "project",
})
mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "children", "folder-design", "children", "project-web", "home.json"), map[string]any{
mustWriteStructured(t, filepath.Join(root, "projects", "project-primary-project", "children", "folder-design", "children", "project-web", testHomeFileName), map[string]any{
"type": "project-home",
"project": "web",
})
mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "children", "folder-design", "children", "project-web", "acl.json"), map[string]any{
mustWriteStructured(t, filepath.Join(root, "projects", "project-primary-project", "children", "folder-design", "children", "project-web", testACLFileName), map[string]any{
"inherits": true,
"rules": []any{},
})
mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "tree", "folder-docs", "folder.json"), map[string]any{
mustWriteStructured(t, filepath.Join(root, "projects", "project-primary-project", "tree", "folder-docs", testFolderFileName), map[string]any{
"name": "Docs",
"slug": "docs",
})
mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "tree", "folder-docs", "item-roadmap", "item.json"), map[string]any{
mustWriteStructured(t, filepath.Join(root, "projects", "project-primary-project", "tree", "folder-docs", "item-roadmap", testItemFileName), map[string]any{
"id": "item-1",
"name": "Roadmap",
"slug": "roadmap",
"type": "board",
})
mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "tree", "folder-docs", "item-roadmap", "schema.json"), map[string]any{
mustWriteStructured(t, filepath.Join(root, "projects", "project-primary-project", "tree", "folder-docs", "item-roadmap", testSchemaFileName), map[string]any{
"type": "object",
})
mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "tree", "folder-docs", "item-roadmap", "data.json"), map[string]any{
mustWriteStructured(t, filepath.Join(root, "projects", "project-primary-project", "tree", "folder-docs", "item-roadmap", testDataFileName), map[string]any{
"title": "Roadmap",
})
mustWriteJSON(t, filepath.Join(root, "users", "settings.json"), map[string]any{
mustWriteStructured(t, filepath.Join(root, "users", testSettingsFileName), map[string]any{
"primaryAdminId": "admin-1",
})
mustWriteJSON(t, filepath.Join(root, "users", "data.json"), map[string]any{
mustWriteStructured(t, filepath.Join(root, "users", testDataFileName), map[string]any{
"users": []map[string]any{{"id": "admin-1", "email": "ronald@example.com"}},
})
@@ -133,7 +172,7 @@ func TestScanRootBuildsProjectedNodesFromBootstrapShape(t *testing.T) {
t.Fatalf("expected root organization slug primary-organization, got %q", rootNode.OrganizationSlug)
}
tenantSettings := index["settings.json"]
tenantSettings := index[testSettingsFileName]
if tenantSettings.LogicalType != "tenant" || tenantSettings.FileRole != "settings" {
t.Fatalf("unexpected tenant settings classification: %#v", tenantSettings)
}
@@ -141,7 +180,31 @@ func TestScanRootBuildsProjectedNodesFromBootstrapShape(t *testing.T) {
t.Fatalf("expected installation id installation-1, got %q", tenantSettings.InstallationID)
}
deptSettings := index["departments/department-primary-department/settings.json"]
packManifest := index[filepath.ToSlash(filepath.Join("catalog", "packs", "pack-core", testManifestFileName))]
if packManifest.LogicalType != "catalog" || packManifest.FileRole != "manifest" {
t.Fatalf("unexpected pack manifest classification: %#v", packManifest)
}
if packManifest.ResourceSlug != "core" {
t.Fatalf("expected pack manifest resource slug core, got %q", packManifest.ResourceSlug)
}
entryManifest := index[filepath.ToSlash(filepath.Join("catalog", "packs", "pack-core", "entries", "app-shell", testManifestFileName))]
if entryManifest.LogicalType != "catalog" || entryManifest.FileRole != "manifest" {
t.Fatalf("unexpected catalog entry manifest classification: %#v", entryManifest)
}
if entryManifest.ResourceID != "app-shell" {
t.Fatalf("expected catalog entry manifest resource id app-shell, got %q", entryManifest.ResourceID)
}
standaloneManifest := index[filepath.ToSlash(filepath.Join("catalog", "standalone", "app-shell", testManifestFileName))]
if standaloneManifest.LogicalType != "catalog" || standaloneManifest.FileRole != "manifest" {
t.Fatalf("unexpected standalone manifest classification: %#v", standaloneManifest)
}
if standaloneManifest.ResourceSlug != "app-shell" {
t.Fatalf("expected standalone manifest resource slug app-shell, got %q", standaloneManifest.ResourceSlug)
}
deptSettings := index[filepath.ToSlash(filepath.Join("departments", "department-primary-department", testSettingsFileName))]
if deptSettings.DepartmentSlug != "primary-department" {
t.Fatalf("expected department slug primary-department, got %q", deptSettings.DepartmentSlug)
}
@@ -149,12 +212,12 @@ func TestScanRootBuildsProjectedNodesFromBootstrapShape(t *testing.T) {
t.Fatalf("expected department resource id dept-1, got %q", deptSettings.ResourceID)
}
teamSettings := index["departments/department-primary-department/teams/team-primary-team/settings.json"]
teamSettings := index[filepath.ToSlash(filepath.Join("departments", "department-primary-department", "teams", "team-primary-team", testSettingsFileName))]
if teamSettings.TeamSlug != "primary-team" {
t.Fatalf("expected team slug primary-team, got %q", teamSettings.TeamSlug)
}
projectSettings := index["projects/project-primary-project/settings.json"]
projectSettings := index[filepath.ToSlash(filepath.Join("projects", "project-primary-project", testSettingsFileName))]
if projectSettings.ProjectSlug != "primary-project" {
t.Fatalf("expected project slug primary-project, got %q", projectSettings.ProjectSlug)
}
@@ -177,12 +240,12 @@ func TestScanRootBuildsProjectedNodesFromBootstrapShape(t *testing.T) {
t.Fatalf("unexpected hierarchy folder node: %#v", hierarchyFolder)
}
hierarchyFolderACL := index["projects/project-primary-project/children/folder-design/acl.json"]
hierarchyFolderACL := index[filepath.ToSlash(filepath.Join("projects", "project-primary-project", "children", "folder-design", testACLFileName))]
if hierarchyFolderACL.LogicalType != "hierarchy_folder" || hierarchyFolderACL.FileRole != "acl" {
t.Fatalf("unexpected hierarchy folder acl classification: %#v", hierarchyFolderACL)
}
childProjectSettings := index["projects/project-primary-project/children/folder-design/children/project-web/settings.json"]
childProjectSettings := index[filepath.ToSlash(filepath.Join("projects", "project-primary-project", "children", "folder-design", "children", "project-web", testSettingsFileName))]
if childProjectSettings.LogicalType != "project" || childProjectSettings.ProjectSlug != "web" {
t.Fatalf("unexpected child project classification: %#v", childProjectSettings)
}
@@ -192,12 +255,12 @@ func TestScanRootBuildsProjectedNodesFromBootstrapShape(t *testing.T) {
t.Fatalf("unexpected tree folder node: %#v", treeFolder)
}
treeFolderACL := index["projects/project-primary-project/tree/folder-docs/folder.json"]
treeFolderACL := index[filepath.ToSlash(filepath.Join("projects", "project-primary-project", "tree", "folder-docs", testFolderFileName))]
if treeFolderACL.LogicalType != "hierarchy_folder" || treeFolderACL.FileRole != "folder" {
t.Fatalf("unexpected tree folder file classification: %#v", treeFolderACL)
}
treeItem := index["projects/project-primary-project/tree/folder-docs/item-roadmap/item.json"]
treeItem := index[filepath.ToSlash(filepath.Join("projects", "project-primary-project", "tree", "folder-docs", "item-roadmap", testItemFileName))]
if treeItem.LogicalType != "item" || treeItem.FileRole != "item" {
t.Fatalf("unexpected tree item classification: %#v", treeItem)
}
@@ -205,12 +268,12 @@ func TestScanRootBuildsProjectedNodesFromBootstrapShape(t *testing.T) {
t.Fatalf("expected tree item resource slug roadmap, got %#v", treeItem.ResourceSlug)
}
usersData := index["users/data.json"]
usersData := index[filepath.ToSlash(filepath.Join("users", testDataFileName))]
if usersData.LogicalType != "users" || usersData.FileRole != "data" {
t.Fatalf("unexpected users data classification: %#v", usersData)
}
if usersData.Checksum == "" || usersData.SizeBytes == 0 {
t.Fatalf("expected users/data.json checksum and size to be populated: %#v", usersData)
t.Fatalf("expected users/%s checksum and size to be populated: %#v", testDataFileName, usersData)
}
}
@@ -221,13 +284,32 @@ func mustMkdirAll(t *testing.T, path string) {
}
}
func mustWriteJSON(t *testing.T, path string, payload any) {
func mustWriteStructured(t *testing.T, path string, payload any) {
t.Helper()
bytes, err := json.MarshalIndent(payload, "", " ")
if err != nil {
t.Fatalf("MarshalIndent(%q) error = %v", path, err)
var (
bytes []byte
err error
)
switch filepath.Ext(path) {
case ".cbor":
bytes, err = cbor.Marshal(payload)
case ".json":
bytes, err = json.MarshalIndent(payload, "", " ")
if err == nil {
bytes = append(bytes, '\n')
}
case ".jsonc":
bytes, err = json.MarshalIndent(payload, "", " ")
if err == nil {
bytes = append([]byte("// Fixture JSONC with comments and trailing commas enabled.\n"), bytes...)
bytes = append(bytes[:len(bytes)-2], []byte(",\n}\n")...)
}
default:
t.Fatalf("unsupported structured fixture file %q", path)
}
if err != nil {
t.Fatalf("marshal %q error = %v", path, err)
}
bytes = append(bytes, '\n')
if err := os.WriteFile(path, bytes, 0o644); err != nil {
t.Fatalf("WriteFile(%q) error = %v", path, err)
}