replaced explicit masterfeats/feat base+lock path branches
This commit is contained in:
+94
-50
@@ -383,22 +383,17 @@ func validateDataObject(path string, obj map[string]any, report *ValidationRepor
|
||||
base := filepath.Base(path)
|
||||
slashed := filepath.ToSlash(path)
|
||||
inModules := strings.Contains(slashed, "/modules/")
|
||||
dataPath := parseTopdataDataPath(path)
|
||||
|
||||
if strings.HasSuffix(slashed, "/data/masterfeats/base.json") {
|
||||
validateMasterfeatsBaseFile(path, obj, report)
|
||||
return
|
||||
}
|
||||
if strings.HasSuffix(slashed, "/data/masterfeats/lock.json") {
|
||||
validateMasterfeatsLockFile(path, obj, report)
|
||||
return
|
||||
}
|
||||
if strings.HasSuffix(slashed, "/data/feat/base.json") {
|
||||
validateFeatBaseFile(path, obj, report)
|
||||
return
|
||||
}
|
||||
if strings.HasSuffix(slashed, "/data/feat/lock.json") {
|
||||
validateFeatLockFile(path, obj, report)
|
||||
return
|
||||
if invariant, ok := datasetInvariantForPath(dataPath); ok {
|
||||
if dataPath.IsRootBase {
|
||||
validateDatasetInvariantBaseFile(path, obj, invariant, report)
|
||||
return
|
||||
}
|
||||
if dataPath.IsRootLock {
|
||||
validateDatasetInvariantLockFile(path, obj, invariant, report)
|
||||
return
|
||||
}
|
||||
}
|
||||
if strings.Contains(slashed, "/data/feat/generated/") {
|
||||
validateFeatGeneratedFile(path, obj, report)
|
||||
@@ -454,21 +449,85 @@ func validateDataObject(path string, obj map[string]any, report *ValidationRepor
|
||||
})
|
||||
}
|
||||
|
||||
func validateMasterfeatsBaseFile(path string, obj map[string]any, report *ValidationReport) {
|
||||
validateRowsFile(path, obj, report, true)
|
||||
validateDerivedBaseOutput(path, obj, "masterfeats", report)
|
||||
type topdataDataPath struct {
|
||||
Segments []string
|
||||
IsRootBase bool
|
||||
IsRootLock bool
|
||||
}
|
||||
|
||||
columns := extractValidationColumns(obj)
|
||||
for _, required := range []string{"LABEL", "STRREF", "DESCRIPTION"} {
|
||||
if _, ok := canonicalColumn(columns, required); !ok {
|
||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||
Severity: SeverityError,
|
||||
Path: path,
|
||||
Message: fmt.Sprintf("masterfeats base.json must include %s in columns", required),
|
||||
})
|
||||
}
|
||||
func parseTopdataDataPath(path string) topdataDataPath {
|
||||
slashed := filepath.ToSlash(path)
|
||||
marker := "/data/"
|
||||
index := strings.LastIndex(slashed, marker)
|
||||
if index == -1 {
|
||||
return topdataDataPath{}
|
||||
}
|
||||
rel := strings.Trim(slashed[index+len(marker):], "/")
|
||||
if rel == "" {
|
||||
return topdataDataPath{}
|
||||
}
|
||||
segments := strings.Split(rel, "/")
|
||||
return topdataDataPath{
|
||||
Segments: segments,
|
||||
IsRootBase: len(segments) == 2 && segments[1] == "base.json",
|
||||
IsRootLock: len(segments) == 2 && segments[1] == "lock.json",
|
||||
}
|
||||
}
|
||||
|
||||
type datasetValidationInvariant struct {
|
||||
Name string
|
||||
KeyPrefix string
|
||||
RequiredColumns []string
|
||||
}
|
||||
|
||||
var datasetValidationInvariants = map[string]datasetValidationInvariant{
|
||||
"feat": {
|
||||
Name: "feat",
|
||||
KeyPrefix: "feat:",
|
||||
},
|
||||
"masterfeats": {
|
||||
Name: "masterfeats",
|
||||
KeyPrefix: "masterfeats:",
|
||||
RequiredColumns: []string{"LABEL", "STRREF", "DESCRIPTION"},
|
||||
},
|
||||
}
|
||||
|
||||
func datasetInvariantForPath(dataPath topdataDataPath) (datasetValidationInvariant, bool) {
|
||||
if len(dataPath.Segments) == 0 {
|
||||
return datasetValidationInvariant{}, false
|
||||
}
|
||||
invariant, ok := datasetValidationInvariants[dataPath.Segments[0]]
|
||||
return invariant, ok
|
||||
}
|
||||
|
||||
func validateDatasetInvariantBaseFile(path string, obj map[string]any, invariant datasetValidationInvariant, report *ValidationReport) {
|
||||
validateRowsFile(path, obj, report, true)
|
||||
validateDerivedBaseOutput(path, obj, invariant.Name, report)
|
||||
validateRequiredColumns(path, obj, invariant, report)
|
||||
validateRowsKeyPrefix(path, obj, invariant, report)
|
||||
}
|
||||
|
||||
func validateRequiredColumns(path string, obj map[string]any, invariant datasetValidationInvariant, report *ValidationReport) {
|
||||
if len(invariant.RequiredColumns) == 0 {
|
||||
return
|
||||
}
|
||||
columns := extractValidationColumns(obj)
|
||||
for _, required := range invariant.RequiredColumns {
|
||||
if _, ok := canonicalColumn(columns, required); ok {
|
||||
continue
|
||||
}
|
||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||
Severity: SeverityError,
|
||||
Path: path,
|
||||
Message: fmt.Sprintf("%s base.json must include %s in columns", invariant.Name, required),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func validateRowsKeyPrefix(path string, obj map[string]any, invariant datasetValidationInvariant, report *ValidationReport) {
|
||||
if invariant.KeyPrefix == "" {
|
||||
return
|
||||
}
|
||||
rows, ok := obj["rows"].([]any)
|
||||
if !ok {
|
||||
return
|
||||
@@ -482,34 +541,32 @@ func validateMasterfeatsBaseFile(path string, obj map[string]any, report *Valida
|
||||
if !ok || strings.TrimSpace(key) == "" {
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(key, "masterfeats:") {
|
||||
if !strings.HasPrefix(key, invariant.KeyPrefix) {
|
||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||
Severity: SeverityError,
|
||||
Path: path,
|
||||
Message: fmt.Sprintf("masterfeats row %d key %q must start with masterfeats:", index, key),
|
||||
Message: fmt.Sprintf("%s row %d key %q must start with %s", invariant.Name, index, key, invariant.KeyPrefix),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateMasterfeatsLockFile(path string, obj map[string]any, report *ValidationReport) {
|
||||
func validateDatasetInvariantLockFile(path string, obj map[string]any, invariant datasetValidationInvariant, report *ValidationReport) {
|
||||
validateLockObject(path, obj, report)
|
||||
if invariant.KeyPrefix == "" {
|
||||
return
|
||||
}
|
||||
for key := range obj {
|
||||
if !strings.HasPrefix(key, "masterfeats:") {
|
||||
if !strings.HasPrefix(key, invariant.KeyPrefix) {
|
||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||
Severity: SeverityError,
|
||||
Path: path,
|
||||
Message: fmt.Sprintf("masterfeats lock key %q must start with masterfeats:", key),
|
||||
Message: fmt.Sprintf("%s lock key %q must start with %s", invariant.Name, key, invariant.KeyPrefix),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateFeatBaseFile(path string, obj map[string]any, report *ValidationReport) {
|
||||
validateRowsFile(path, obj, report, true)
|
||||
validateDerivedBaseOutput(path, obj, "feat", report)
|
||||
}
|
||||
|
||||
func validateDerivedBaseOutput(path string, obj map[string]any, datasetLabel string, report *ValidationReport) {
|
||||
if output, ok := obj["output"]; !ok {
|
||||
return
|
||||
@@ -522,19 +579,6 @@ func validateDerivedBaseOutput(path string, obj map[string]any, datasetLabel str
|
||||
}
|
||||
}
|
||||
|
||||
func validateFeatLockFile(path string, obj map[string]any, report *ValidationReport) {
|
||||
validateLockObject(path, obj, report)
|
||||
for key := range obj {
|
||||
if !strings.HasPrefix(key, "feat:") {
|
||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||
Severity: SeverityError,
|
||||
Path: path,
|
||||
Message: fmt.Sprintf("feat lock key %q must start with feat:", key),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateFeatGeneratedFile(path string, obj map[string]any, report *ValidationReport) {
|
||||
family, ok := obj["family"].(string)
|
||||
if !ok || strings.TrimSpace(family) == "" {
|
||||
|
||||
@@ -79,6 +79,41 @@ func TestValidateAndBuildDerivesFeatOutputWhenOmitted(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateProjectRejectsInvalidFeatInvariantContract(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "feat"))
|
||||
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "feat", "base.json"), `{
|
||||
"output": "masterfeats.2da",
|
||||
"columns": ["LABEL", "FEAT", "DESCRIPTION"],
|
||||
"rows": [
|
||||
{
|
||||
"id": 0,
|
||||
"key": "masterfeats:test",
|
||||
"LABEL": "FEAT_TEST",
|
||||
"FEAT": "100",
|
||||
"DESCRIPTION": "200"
|
||||
}
|
||||
]
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "feat", "lock.json"), `{"masterfeats:test":0}`+"\n")
|
||||
|
||||
report := ValidateProject(testProject(root))
|
||||
if !report.HasErrors() {
|
||||
t.Fatal("expected validation errors for invalid feat invariants")
|
||||
}
|
||||
text := diagnosticsText(report.Diagnostics)
|
||||
for _, want := range []string{
|
||||
"feat output must be feat.2da",
|
||||
`feat row 0 key "masterfeats:test" must start with feat:`,
|
||||
`feat lock key "masterfeats:test" must start with feat:`,
|
||||
} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Fatalf("expected validation message %q, got:\n%s", want, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateProjectRejectsDuplicateJSONKeys(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "repadjust"))
|
||||
|
||||
Reference in New Issue
Block a user