feat(topdata): reject unsupported canonical json properties

This commit is contained in:
2026-06-25 22:26:04 +02:00
parent 1c32b6c423
commit b7464bda4f
2 changed files with 520 additions and 40 deletions
+65 -40
View File
@@ -333,6 +333,17 @@ func validateDuplicateJSONKeys(path string, raw []byte, report *ValidationReport
}
}
func reportTopdataContractError(path string, err error, report *ValidationReport) {
if err == nil {
return
}
report.Diagnostics = append(report.Diagnostics, Diagnostic{
Severity: SeverityError,
Path: path,
Message: err.Error(),
})
}
func scanJSONValueForDuplicateKeys(decoder *json.Decoder, path string, report *ValidationReport) error {
token, err := decoder.Token()
if err != nil {
@@ -425,35 +436,40 @@ func validateDataObject(path string, obj map[string]any, report *ValidationRepor
return
}
if base == "base.json" {
reportTopdataContractError(path, unsupportedObjectKeysError("base.json root", obj, baseOrPlainRootKeys...), report)
validateRowsFile(path, obj, report, true)
return
}
if inModules {
if _, hasEntries := obj["entries"]; hasEntries {
validateEntriesFile(path, obj, report)
return
}
if _, hasOverrides := obj["overrides"]; hasOverrides {
validateOverridesFile(path, obj, report)
return
}
if _, hasRows := obj["rows"]; hasRows {
validateRowsFile(path, obj, report, false)
return
}
if _, hasColumns := obj["columns"]; hasColumns {
reportTopdataContractError(path, unsupportedObjectKeysError("module root", obj, canonicalModuleRootKeys...), report)
_, hasColumns := obj["columns"]
_, hasEntries := obj["entries"]
_, hasOverrides := obj["overrides"]
_, hasRows := obj["rows"]
if hasColumns {
validateColumnsFile(path, obj, report)
return
}
report.Diagnostics = append(report.Diagnostics, Diagnostic{
Severity: SeverityWarning,
Path: path,
Message: "module file does not use a recognized canonical shape yet; expected one of columns, entries, overrides, or rows",
})
if hasEntries {
validateEntriesFileWithColumns(path, obj, report, false)
}
if hasOverrides {
validateOverridesFileWithColumns(path, obj, report, false)
}
if hasRows {
validateRowsFileWithColumns(path, obj, report, false, false)
}
if !hasColumns && !hasEntries && !hasOverrides && !hasRows {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
Severity: SeverityWarning,
Path: path,
Message: "module file does not use a recognized canonical shape yet; expected one of columns, entries, overrides, or rows",
})
}
return
}
if _, hasRows := obj["rows"]; hasRows {
reportTopdataContractError(path, unsupportedObjectKeysError("plain rows root", obj, baseOrPlainRootKeys...), report)
validateRowsFile(path, obj, report, false)
return
}
@@ -826,6 +842,10 @@ func validateLockObject(path string, obj map[string]any, report *ValidationRepor
}
func validateRowsFile(path string, obj map[string]any, report *ValidationReport, requireColumns bool) {
validateRowsFileWithColumns(path, obj, report, requireColumns, true)
}
func validateRowsFileWithColumns(path string, obj map[string]any, report *ValidationReport, requireColumns bool, validateColumns bool) {
rows, ok := obj["rows"]
if !ok {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
@@ -845,7 +865,7 @@ func validateRowsFile(path string, obj map[string]any, report *ValidationReport,
if rowsList, ok := rows.([]any); ok {
validateRowCollection(path, obj, rowsList, report)
}
if _, ok := obj["columns"]; ok {
if _, ok := obj["columns"]; ok && validateColumns {
validateColumnsFile(path, obj, report)
} else if requireColumns {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
@@ -896,6 +916,10 @@ func validateColumnsFile(path string, obj map[string]any, report *ValidationRepo
}
func validateEntriesFile(path string, obj map[string]any, report *ValidationReport) {
validateEntriesFileWithColumns(path, obj, report, true)
}
func validateEntriesFileWithColumns(path string, obj map[string]any, report *ValidationReport, validateColumns bool) {
entries, ok := obj["entries"]
if !ok {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
@@ -931,12 +955,16 @@ func validateEntriesFile(path string, obj map[string]any, report *ValidationRepo
validateInlineTextUsage(path, key, entry, report)
}
}
if _, ok := obj["columns"]; ok {
if _, ok := obj["columns"]; ok && validateColumns {
validateColumnsFile(path, obj, report)
}
}
func validateOverridesFile(path string, obj map[string]any, report *ValidationReport) {
validateOverridesFileWithColumns(path, obj, report, true)
}
func validateOverridesFileWithColumns(path string, obj map[string]any, report *ValidationReport, validateColumns bool) {
overrides, ok := obj["overrides"]
if !ok {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
@@ -957,12 +985,13 @@ func validateOverridesFile(path string, obj map[string]any, report *ValidationRe
container := map[string]any{"rows": overrideList}
validateRowCollection(path, container, overrideList, report)
}
if _, ok := obj["columns"]; ok {
if _, ok := obj["columns"]; ok && validateColumns {
validateColumnsFile(path, obj, report)
}
}
func validateGlobalJSONFile(path string, obj map[string]any, report *ValidationReport) {
reportTopdataContractError(path, unsupportedObjectKeysError("global.json root", obj, globalRootKeys...), report)
if _, ok := obj["columns"]; ok {
validateColumnsFile(path, obj, report)
}
@@ -1019,6 +1048,8 @@ func validateGlobalJSONFile(path string, obj map[string]any, report *ValidationR
})
continue
}
err := unsupportedObjectKeysError(fmt.Sprintf("global injection %d", index), injection, globalInjectionKeys...)
reportTopdataContractError(path, err, report)
row, ok := injection["row"].(map[string]any)
if !ok {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
@@ -1029,8 +1060,10 @@ func validateGlobalJSONFile(path string, obj map[string]any, report *ValidationR
} else {
rows = append(rows, row)
}
validateGlobalConditionList(path, fmt.Sprintf("global injection %d require_present", index), injection["require_present"], report)
validateGlobalConditionList(path, fmt.Sprintf("global injection %d unless_present", index), injection["unless_present"], report)
if err == nil {
_, conditionErr := parseGlobalConditionGroups(injection)
reportTopdataContractError(path, conditionErr, report)
}
}
validateRowCollection(path, obj, rows, report)
}
@@ -1099,6 +1132,7 @@ func validateGlobalDefaults(path string, obj map[string]any, report *ValidationR
})
continue
}
reportTopdataContractError(path, unsupportedObjectKeysError(fmt.Sprintf("global default %d", index), defaultRule, globalDefaultRuleKeys...), report)
validateGlobalDefaultMatch(path, index, defaultRule["match"], report)
rawValues, ok := defaultRule["values"]
if !ok {
@@ -1155,14 +1189,12 @@ func validateGlobalDefaultMatch(path string, index int, rawMatch any, report *Va
})
return
}
for key := range match {
if key != "source" {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
Severity: SeverityError,
Path: path,
Message: fmt.Sprintf("global default %d match.%s is not supported", index, key),
})
}
for _, key := range unsupportedObjectKeys(match, globalDefaultMatchKeys...) {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
Severity: SeverityError,
Path: path,
Message: fmt.Sprintf("global default %d match.%s is not supported (unsupported key; supported keys: %s)", index, key, strings.Join(globalDefaultMatchKeys, ", ")),
})
}
source, ok := match["source"].(string)
if !ok || strings.TrimSpace(source) == "" {
@@ -1193,14 +1225,7 @@ func validateGlobalDefaultValue(path string, index int, field string, value any,
if !hasFormat {
return
}
if len(obj) != 1 {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
Severity: SeverityError,
Path: path,
Message: fmt.Sprintf("global default %d values.%s format object must contain only format", index, field),
})
return
}
reportTopdataContractError(path, unsupportedObjectKeysError(fmt.Sprintf("global default %d values.%s", index, field), obj, globalDefaultFormatKeys...), report)
format, ok := rawFormat.(string)
if !ok {
report.Diagnostics = append(report.Diagnostics, Diagnostic{