feat(topdata): add any-present injection conditions
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package topdata
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"slices"
|
||||
@@ -680,6 +682,466 @@ func TestValidateProjectAcceptsCanonicalJSONContracts(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildNativeAnyPresent(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
rows string
|
||||
wantInjected bool
|
||||
}{
|
||||
{
|
||||
name: "first alternative",
|
||||
rows: `[
|
||||
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"}
|
||||
]`,
|
||||
wantInjected: true,
|
||||
},
|
||||
{
|
||||
name: "later alternative",
|
||||
rows: `[
|
||||
{"id": 0, "SkillLabel": "Tumble", "SkillIndex": {"id": "skills:tumble"}, "ClassSkill": "1"}
|
||||
]`,
|
||||
wantInjected: true,
|
||||
},
|
||||
{
|
||||
name: "no alternatives",
|
||||
rows: `[
|
||||
{"id": 0, "SkillLabel": "Concentration", "SkillIndex": {"id": "skills:concentration"}, "ClassSkill": "1"}
|
||||
]`,
|
||||
wantInjected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
writeCanonicalPlainSkillsFixture(t, root, tc.rows, `{
|
||||
"injections": [
|
||||
{
|
||||
"row": {
|
||||
"SkillLabel": "ProfessionFarmer",
|
||||
"SkillIndex": {"id": "skills:profession_farmer"},
|
||||
"ClassSkill": "1"
|
||||
},
|
||||
"any_present": [
|
||||
{"field": "SkillIndex", "id": "skills:athletics"},
|
||||
{"field": "SkillIndex", "id": "skills:tumble"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`+"\n")
|
||||
|
||||
proj := testProject(root)
|
||||
proj.Config.TopData.ReferenceBuilder = ""
|
||||
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildNativeWithOptions failed: %v", err)
|
||||
}
|
||||
|
||||
table := read2DATable(t, filepath.Join(result.Output2DADir, "cls_skill_guard.2da"))
|
||||
if got := tableContainsCellValue(table, "SkillLabel", "ProfessionFarmer"); got != tc.wantInjected {
|
||||
t.Fatalf("ProfessionFarmer presence = %v, want %v", got, tc.wantInjected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildNativeCombinesGlobalConditionGroups(t *testing.T) {
|
||||
t.Run("require_present and any_present", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
rows string
|
||||
wantInjected bool
|
||||
}{
|
||||
{
|
||||
name: "both pass",
|
||||
rows: `[
|
||||
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"},
|
||||
{"id": 1, "SkillLabel": "Tumble", "SkillIndex": {"id": "skills:tumble"}, "ClassSkill": "1"}
|
||||
]`,
|
||||
wantInjected: true,
|
||||
},
|
||||
{
|
||||
name: "require_present fails",
|
||||
rows: `[
|
||||
{"id": 0, "SkillLabel": "Tumble", "SkillIndex": {"id": "skills:tumble"}, "ClassSkill": "1"}
|
||||
]`,
|
||||
wantInjected: false,
|
||||
},
|
||||
{
|
||||
name: "any_present fails",
|
||||
rows: `[
|
||||
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"}
|
||||
]`,
|
||||
wantInjected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
writeCanonicalPlainSkillsFixture(t, root, tc.rows, `{
|
||||
"injections": [
|
||||
{
|
||||
"row": {
|
||||
"SkillLabel": "DualGate",
|
||||
"SkillIndex": {"id": "skills:lore"},
|
||||
"ClassSkill": "1"
|
||||
},
|
||||
"require_present": [
|
||||
{"field": "SkillIndex", "id": "skills:athletics"}
|
||||
],
|
||||
"any_present": [
|
||||
{"field": "SkillIndex", "id": "skills:tumble"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`+"\n")
|
||||
|
||||
proj := testProject(root)
|
||||
proj.Config.TopData.ReferenceBuilder = ""
|
||||
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildNativeWithOptions failed: %v", err)
|
||||
}
|
||||
|
||||
table := read2DATable(t, filepath.Join(result.Output2DADir, "cls_skill_guard.2da"))
|
||||
if got := tableContainsCellValue(table, "SkillLabel", "DualGate"); got != tc.wantInjected {
|
||||
t.Fatalf("DualGate presence = %v, want %v", got, tc.wantInjected)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("any_present and unless_present", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
rows string
|
||||
wantInjected bool
|
||||
}{
|
||||
{
|
||||
name: "both pass",
|
||||
rows: `[
|
||||
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"}
|
||||
]`,
|
||||
wantInjected: true,
|
||||
},
|
||||
{
|
||||
name: "any_present fails",
|
||||
rows: `[
|
||||
{"id": 0, "SkillLabel": "Concentration", "SkillIndex": {"id": "skills:concentration"}, "ClassSkill": "1"}
|
||||
]`,
|
||||
wantInjected: false,
|
||||
},
|
||||
{
|
||||
name: "unless_present fails",
|
||||
rows: `[
|
||||
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"},
|
||||
{"id": 1, "SkillLabel": "ProfessionFarmer", "SkillIndex": {"id": "skills:profession_farmer"}, "ClassSkill": "1"}
|
||||
]`,
|
||||
wantInjected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
writeCanonicalPlainSkillsFixture(t, root, tc.rows, `{
|
||||
"injections": [
|
||||
{
|
||||
"row": {
|
||||
"SkillLabel": "NeedsAnyWithoutBlocked",
|
||||
"SkillIndex": {"id": "skills:lore"},
|
||||
"ClassSkill": "1"
|
||||
},
|
||||
"any_present": [
|
||||
{"field": "SkillIndex", "id": "skills:athletics"}
|
||||
],
|
||||
"unless_present": [
|
||||
{"field": "SkillIndex", "id": "skills:profession_farmer"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`+"\n")
|
||||
|
||||
proj := testProject(root)
|
||||
proj.Config.TopData.ReferenceBuilder = ""
|
||||
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildNativeWithOptions failed: %v", err)
|
||||
}
|
||||
|
||||
table := read2DATable(t, filepath.Join(result.Output2DADir, "cls_skill_guard.2da"))
|
||||
if got := tableContainsCellValue(table, "SkillLabel", "NeedsAnyWithoutBlocked"); got != tc.wantInjected {
|
||||
t.Fatalf("NeedsAnyWithoutBlocked presence = %v, want %v", got, tc.wantInjected)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("require_present keeps all semantics", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
rows string
|
||||
wantInjected bool
|
||||
}{
|
||||
{
|
||||
name: "all present",
|
||||
rows: `[
|
||||
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"},
|
||||
{"id": 1, "SkillLabel": "Tumble", "SkillIndex": {"id": "skills:tumble"}, "ClassSkill": "1"}
|
||||
]`,
|
||||
wantInjected: true,
|
||||
},
|
||||
{
|
||||
name: "missing one required row",
|
||||
rows: `[
|
||||
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"}
|
||||
]`,
|
||||
wantInjected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
writeCanonicalPlainSkillsFixture(t, root, tc.rows, `{
|
||||
"injections": [
|
||||
{
|
||||
"row": {
|
||||
"SkillLabel": "AllRequired",
|
||||
"SkillIndex": {"id": "skills:lore"},
|
||||
"ClassSkill": "1"
|
||||
},
|
||||
"require_present": [
|
||||
{"field": "SkillIndex", "id": "skills:athletics"},
|
||||
{"field": "SkillIndex", "id": "skills:tumble"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`+"\n")
|
||||
|
||||
proj := testProject(root)
|
||||
proj.Config.TopData.ReferenceBuilder = ""
|
||||
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildNativeWithOptions failed: %v", err)
|
||||
}
|
||||
|
||||
table := read2DATable(t, filepath.Join(result.Output2DADir, "cls_skill_guard.2da"))
|
||||
if got := tableContainsCellValue(table, "SkillLabel", "AllRequired"); got != tc.wantInjected {
|
||||
t.Fatalf("AllRequired presence = %v, want %v", got, tc.wantInjected)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unless_present keeps none semantics", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
rows string
|
||||
wantInjected bool
|
||||
}{
|
||||
{
|
||||
name: "none present",
|
||||
rows: `[
|
||||
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"}
|
||||
]`,
|
||||
wantInjected: true,
|
||||
},
|
||||
{
|
||||
name: "one blocked row present",
|
||||
rows: `[
|
||||
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"},
|
||||
{"id": 1, "SkillLabel": "ProfessionFarmer", "SkillIndex": {"id": "skills:profession_farmer"}, "ClassSkill": "1"}
|
||||
]`,
|
||||
wantInjected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
writeCanonicalPlainSkillsFixture(t, root, tc.rows, `{
|
||||
"injections": [
|
||||
{
|
||||
"row": {
|
||||
"SkillLabel": "NoneBlocked",
|
||||
"SkillIndex": {"id": "skills:lore"},
|
||||
"ClassSkill": "1"
|
||||
},
|
||||
"unless_present": [
|
||||
{"field": "SkillIndex", "id": "skills:profession_farmer"},
|
||||
{"field": "SkillIndex", "id": "skills:stealth"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`+"\n")
|
||||
|
||||
proj := testProject(root)
|
||||
proj.Config.TopData.ReferenceBuilder = ""
|
||||
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildNativeWithOptions failed: %v", err)
|
||||
}
|
||||
|
||||
table := read2DATable(t, filepath.Join(result.Output2DADir, "cls_skill_guard.2da"))
|
||||
if got := tableContainsCellValue(table, "SkillLabel", "NoneBlocked"); got != tc.wantInjected {
|
||||
t.Fatalf("NoneBlocked presence = %v, want %v", got, tc.wantInjected)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildNativeUsesCurrentRowsForAnyPresent(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
writeCanonicalPlainSkillsFixture(t, root, `[
|
||||
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"}
|
||||
]`, `{
|
||||
"injections": [
|
||||
{
|
||||
"row": {
|
||||
"SkillLabel": "ProfessionFarmer",
|
||||
"SkillIndex": {"id": "skills:profession_farmer"},
|
||||
"ClassSkill": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"row": {
|
||||
"SkillLabel": "Lore",
|
||||
"SkillIndex": {"id": "skills:lore"},
|
||||
"ClassSkill": "1"
|
||||
},
|
||||
"any_present": [
|
||||
{"field": "SkillIndex", "id": "skills:profession_farmer"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`+"\n")
|
||||
|
||||
proj := testProject(root)
|
||||
proj.Config.TopData.ReferenceBuilder = ""
|
||||
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildNativeWithOptions failed: %v", err)
|
||||
}
|
||||
|
||||
table := read2DATable(t, filepath.Join(result.Output2DADir, "cls_skill_guard.2da"))
|
||||
for _, label := range []string{"ProfessionFarmer", "Lore"} {
|
||||
if !tableContainsCellValue(table, "SkillLabel", label) {
|
||||
t.Fatalf("expected %s injection to apply after earlier mutation", label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildNativeRejectsUnsupportedGlobalControlSyntax(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
globalJSON string
|
||||
fragments []string
|
||||
}{
|
||||
{
|
||||
name: "when_present injection key",
|
||||
globalJSON: `{
|
||||
"injections": [
|
||||
{
|
||||
"row": {"ImpactScript": "ss_new_spell"},
|
||||
"when_present": []
|
||||
}
|
||||
]
|
||||
}` + "\n",
|
||||
fragments: []string{"global injection 0", "when_present", "unsupported"},
|
||||
},
|
||||
{
|
||||
name: "unknown condition key",
|
||||
globalJSON: `{
|
||||
"injections": [
|
||||
{
|
||||
"row": {"ImpactScript": "ss_new_spell"},
|
||||
"require_present": [
|
||||
{"field": "FeatIndex", "id": "feat:gate", "typo": true}
|
||||
]
|
||||
}
|
||||
]
|
||||
}` + "\n",
|
||||
fragments: []string{"require_present[0]", "typo", "unsupported"},
|
||||
},
|
||||
{
|
||||
name: "empty any_present",
|
||||
globalJSON: `{
|
||||
"injections": [
|
||||
{
|
||||
"row": {"ImpactScript": "ss_new_spell"},
|
||||
"any_present": []
|
||||
}
|
||||
]
|
||||
}` + "\n",
|
||||
fragments: []string{"any_present", "at least one condition"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
writeCanonicalContractScaffold(t, root)
|
||||
writeCanonicalSpellsDataset(t, root)
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "global.json"), tc.globalJSON)
|
||||
|
||||
proj := testProject(root)
|
||||
proj.Config.TopData.ReferenceBuilder = ""
|
||||
_, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected BuildNativeWithOptions to fail")
|
||||
}
|
||||
for _, fragment := range tc.fragments {
|
||||
if !strings.Contains(err.Error(), fragment) {
|
||||
t.Fatalf("error %q does not contain %q", err, fragment)
|
||||
}
|
||||
}
|
||||
if _, statErr := os.Stat(filepath.Join(compiled2DAOutputDir(proj), "spells.2da")); !errors.Is(statErr, os.ErrNotExist) {
|
||||
t.Fatalf("expected spells.2da to remain absent, got err %v", statErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildNativeRejectsUnsupportedCanonicalRoot(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
writeCanonicalContractScaffold(t, root)
|
||||
writeCanonicalSpellsBase(t, root, `{
|
||||
"output": "spells.2da",
|
||||
"columns": ["Label"],
|
||||
"rows": [
|
||||
{"id": 0, "key": "spells:acid_fog", "Label": "AcidFog"}
|
||||
],
|
||||
"typo": true
|
||||
}`+"\n")
|
||||
|
||||
proj := testProject(root)
|
||||
proj.Config.TopData.ReferenceBuilder = ""
|
||||
_, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected BuildNativeWithOptions to fail")
|
||||
}
|
||||
for _, fragment := range []string{"base.json root", "typo", "unsupported"} {
|
||||
if !strings.Contains(err.Error(), fragment) {
|
||||
t.Fatalf("error %q does not contain %q", err, fragment)
|
||||
}
|
||||
}
|
||||
if _, statErr := os.Stat(filepath.Join(compiled2DAOutputDir(proj), "spells.2da")); !errors.Is(statErr, os.ErrNotExist) {
|
||||
t.Fatalf("expected spells.2da to remain absent, got err %v", statErr)
|
||||
}
|
||||
}
|
||||
|
||||
func tableContainsCellValue(table twoDATable, column, want string) bool {
|
||||
for _, row := range table.rows {
|
||||
if row[column] == want {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func writeCanonicalContractScaffold(t *testing.T, root string) {
|
||||
t.Helper()
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data"))
|
||||
@@ -717,6 +1179,40 @@ func writeCanonicalRepadjustDataset(t *testing.T, root string) {
|
||||
}`+"\n")
|
||||
}
|
||||
|
||||
func writeCanonicalPlainSkillsFixture(t *testing.T, root, datasetRows, globalJSON string) {
|
||||
t.Helper()
|
||||
writeCanonicalContractScaffold(t, root)
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "classes", "skills"))
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "skills", "global.json"), globalJSON)
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "skills", "guard.json"), `{
|
||||
"key": "classes/skills:guard",
|
||||
"output": "cls_skill_guard.2da",
|
||||
"columns": ["SkillLabel", "SkillIndex", "ClassSkill"],
|
||||
"rows": `+datasetRows+`
|
||||
}`+"\n")
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "skills"))
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "skills", "base.json"), `{
|
||||
"output": "skills.2da",
|
||||
"columns": ["Label"],
|
||||
"rows": [
|
||||
{"id": 1, "key": "skills:athletics", "Label": "Athletics"},
|
||||
{"id": 2, "key": "skills:tumble", "Label": "Tumble"},
|
||||
{"id": 3, "key": "skills:concentration", "Label": "Concentration"},
|
||||
{"id": 4, "key": "skills:profession_farmer", "Label": "ProfessionFarmer"},
|
||||
{"id": 5, "key": "skills:lore", "Label": "Lore"},
|
||||
{"id": 6, "key": "skills:stealth", "Label": "Stealth"}
|
||||
]
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "skills", "lock.json"), `{
|
||||
"skills:athletics": 1,
|
||||
"skills:tumble": 2,
|
||||
"skills:concentration": 3,
|
||||
"skills:profession_farmer": 4,
|
||||
"skills:lore": 5,
|
||||
"skills:stealth": 6
|
||||
}`+"\n")
|
||||
}
|
||||
|
||||
func assertDiagnosticTextContainsAll(t *testing.T, diags []Diagnostic, fragments ...string) {
|
||||
t.Helper()
|
||||
text := diagnosticsText(diags)
|
||||
|
||||
@@ -3886,48 +3886,11 @@ func globalManifestPosition(module nativeGeneratedModule) (string, error) {
|
||||
}
|
||||
|
||||
func globalInjectionConditionsMatch(injection map[string]any, rows []map[string]any) (bool, error) {
|
||||
if rawRequired, ok := injection["require_present"]; ok {
|
||||
matches, err := globalConditionListMatches("require_present", rawRequired, rows, true)
|
||||
if err != nil || !matches {
|
||||
return matches, err
|
||||
groups, err := parseGlobalConditionGroups(injection)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
if rawBlocked, ok := injection["unless_present"]; ok {
|
||||
matches, err := globalConditionListMatches("unless_present", rawBlocked, rows, false)
|
||||
if err != nil || !matches {
|
||||
return matches, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func globalConditionListMatches(name string, raw any, rows []map[string]any, required bool) (bool, error) {
|
||||
conditions, ok := raw.([]any)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("%s must be an array", name)
|
||||
}
|
||||
for index, rawCondition := range conditions {
|
||||
condition, ok := rawCondition.(map[string]any)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("%s[%d] must be an object", name, index)
|
||||
}
|
||||
field, _ := condition["field"].(string)
|
||||
id, _ := condition["id"].(string)
|
||||
if strings.TrimSpace(field) == "" {
|
||||
return false, fmt.Errorf("%s[%d].field is required", name, index)
|
||||
}
|
||||
if strings.TrimSpace(id) == "" {
|
||||
return false, fmt.Errorf("%s[%d].id is required", name, index)
|
||||
}
|
||||
found := globalReferencePresent(rows, field, id)
|
||||
if required && !found {
|
||||
return false, nil
|
||||
}
|
||||
if !required && found {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
return globalConditionGroupsMatch(groups, rows), nil
|
||||
}
|
||||
|
||||
func globalRowIdentityPresent(row map[string]any, rows []map[string]any) bool {
|
||||
|
||||
Reference in New Issue
Block a user