Warn when circumventing class spellbooks
This commit is contained in:
@@ -47,6 +47,9 @@ func validateClassSpellbooks(dataDir string, report *ValidationReport) {
|
|||||||
}
|
}
|
||||||
_, diagnostics := classSpellbookPlans(dataDir, collected)
|
_, diagnostics := classSpellbookPlans(dataDir, collected)
|
||||||
report.Diagnostics = append(report.Diagnostics, diagnostics...)
|
report.Diagnostics = append(report.Diagnostics, diagnostics...)
|
||||||
|
if len(diagnostics) == 0 {
|
||||||
|
warnSpellModulesForManagedSpellbookColumns(dataDir, collected, report)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectSpellbookValidationDatasets(dataDir string) ([]nativeCollectedDataset, error) {
|
func collectSpellbookValidationDatasets(dataDir string) ([]nativeCollectedDataset, error) {
|
||||||
@@ -187,6 +190,81 @@ func classSpellbookPlans(dataDir string, collected []nativeCollectedDataset) ([]
|
|||||||
return plans, diagnostics
|
return plans, diagnostics
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func warnSpellModulesForManagedSpellbookColumns(dataDir string, collected []nativeCollectedDataset, report *ValidationReport) {
|
||||||
|
plans, diagnostics := classSpellbookPlans(dataDir, collected)
|
||||||
|
if len(diagnostics) > 0 || len(plans) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
managed := map[string]classSpellbookPlan{}
|
||||||
|
for _, plan := range plans {
|
||||||
|
managed[strings.ToLower(plan.Column)] = plan
|
||||||
|
}
|
||||||
|
paths, err := collectModulePaths(filepath.Join(dataDir, "spells", "modules"))
|
||||||
|
if err != nil {
|
||||||
|
report.Diagnostics = append(report.Diagnostics, Diagnostic{Severity: SeverityError, Path: dataDir, Message: fmt.Sprintf("collect spell modules for spellbook warnings: %v", err)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, path := range paths {
|
||||||
|
obj, err := loadJSONObject(path)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
warnSpellbookManagedColumnsInValue(dataDir, path, obj["entries"], managed, report)
|
||||||
|
warnSpellbookManagedColumnsInValue(dataDir, path, obj["overrides"], managed, report)
|
||||||
|
warnSpellbookManagedColumnsInValue(dataDir, path, obj["rows"], managed, report)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func warnSpellbookManagedColumnsInValue(dataDir, path string, value any, managed map[string]classSpellbookPlan, report *ValidationReport) {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
for _, key := range sortedKeys(typed) {
|
||||||
|
row, ok := typed[key].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
warnSpellbookManagedColumnsInRow(dataDir, path, row, managed, report)
|
||||||
|
}
|
||||||
|
case []any:
|
||||||
|
for _, raw := range typed {
|
||||||
|
row, ok := raw.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
warnSpellbookManagedColumnsInRow(dataDir, path, row, managed, report)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func warnSpellbookManagedColumnsInRow(dataDir, path string, row map[string]any, managed map[string]classSpellbookPlan, report *ValidationReport) {
|
||||||
|
for field, value := range row {
|
||||||
|
plan, ok := managed[strings.ToLower(field)]
|
||||||
|
if !ok || isNullLikeValue(value) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
spellbookPath := displayTopdataPath(dataDir, plan.Spec.Path)
|
||||||
|
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||||
|
Severity: SeverityWarning,
|
||||||
|
Path: path,
|
||||||
|
Message: fmt.Sprintf(
|
||||||
|
"spell module authors class spell column %q managed by class spellbook %s; use %s instead",
|
||||||
|
plan.Column,
|
||||||
|
spellbookPath,
|
||||||
|
spellbookPath,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func displayTopdataPath(dataDir, path string) string {
|
||||||
|
topdataDir := filepath.Dir(dataDir)
|
||||||
|
rel, err := filepath.Rel(filepath.Dir(topdataDir), path)
|
||||||
|
if err != nil {
|
||||||
|
return filepath.ToSlash(path)
|
||||||
|
}
|
||||||
|
return filepath.ToSlash(rel)
|
||||||
|
}
|
||||||
|
|
||||||
func loadClassSpellbookSpecs(dir string) ([]classSpellbookSpec, []Diagnostic) {
|
func loadClassSpellbookSpecs(dir string) ([]classSpellbookSpec, []Diagnostic) {
|
||||||
paths, err := collectModulePaths(dir)
|
paths, err := collectModulePaths(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -13626,6 +13626,41 @@ func TestValidateProjectRejectsInvalidClassSpellbooks(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateProjectWarnsWhenSpellModuleAuthorsManagedSpellbookColumn(t *testing.T) {
|
||||||
|
root := testProjectRoot(t)
|
||||||
|
writeMinimalSpellbookProject(t, root)
|
||||||
|
mkdirAll(t, filepath.Join(root, "topdata", "data", "spells", "modules"))
|
||||||
|
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "spellbooks", "bard.json"), `{
|
||||||
|
"key": "classes/spellbooks:bard",
|
||||||
|
"class": "classes:bard",
|
||||||
|
"levels": {
|
||||||
|
"0": ["spells:flare"]
|
||||||
|
}
|
||||||
|
}`+"\n")
|
||||||
|
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "modules", "direct_class_column.json"), `{
|
||||||
|
"columns": ["Sorcerer"],
|
||||||
|
"entries": {
|
||||||
|
"spells:module_spell": {
|
||||||
|
"Label": "ModuleSpell",
|
||||||
|
"Bard": 1,
|
||||||
|
"Sorcerer": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`+"\n")
|
||||||
|
|
||||||
|
report := ValidateProject(testProject(root))
|
||||||
|
if report.HasErrors() {
|
||||||
|
t.Fatalf("expected warning-only validation result, got errors:\n%s", diagnosticsText(report.Diagnostics))
|
||||||
|
}
|
||||||
|
text := diagnosticsText(report.Diagnostics)
|
||||||
|
if !strings.Contains(text, `spell module authors class spell column "Bard" managed by class spellbook`) {
|
||||||
|
t.Fatalf("expected managed Bard column warning, got:\n%s", text)
|
||||||
|
}
|
||||||
|
if !strings.Contains(text, `use topdata/data/classes/spellbooks/bard.json instead`) {
|
||||||
|
t.Fatalf("expected warning to point at spellbook file, got:\n%s", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestValidateProjectDoesNotRewritePlainDatasetLockfile(t *testing.T) {
|
func TestValidateProjectDoesNotRewritePlainDatasetLockfile(t *testing.T) {
|
||||||
root := testProjectRoot(t)
|
root := testProjectRoot(t)
|
||||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "ruleset"))
|
mkdirAll(t, filepath.Join(root, "topdata", "data", "ruleset"))
|
||||||
|
|||||||
Reference in New Issue
Block a user