package topdata import ( "fmt" "slices" "strings" ) type globalCondition struct { Field string ID string } type globalConditionGroups struct { RequirePresent []globalCondition AnyPresent []globalCondition UnlessPresent []globalCondition } var ( baseOrPlainRootKeys = []string{ "output", "key", "columns", "rows", "compare_reference", } canonicalModuleRootKeys = []string{ "output", "key", "columns", "entries", "overrides", "rows", } globalRootKeys = []string{ "columns", "entries", "overrides", "defaults", "position", "injections", } globalInjectionKeys = []string{ "row", "require_present", "any_present", "unless_present", } globalConditionKeys = []string{"field", "id"} globalDefaultRuleKeys = []string{"match", "values"} globalDefaultMatchKeys = []string{"source"} globalDefaultFormatKeys = []string{"format"} ) func unsupportedObjectKeys(obj map[string]any, supported ...string) []string { if len(obj) == 0 { return nil } supportedSet := make(map[string]struct{}, len(supported)) for _, key := range supported { supportedSet[key] = struct{}{} } unsupported := make([]string, 0, len(obj)) for key := range obj { if _, ok := supportedSet[key]; ok { continue } unsupported = append(unsupported, key) } slices.Sort(unsupported) return unsupported } func unsupportedObjectKeysError(label string, obj map[string]any, supported ...string) error { unsupported := unsupportedObjectKeys(obj, supported...) if len(unsupported) == 0 { return nil } return fmt.Errorf("%s contains unsupported key %q (supported keys: %s)", label, unsupported[0], strings.Join(supported, ", ")) } func parseGlobalConditionGroups(injection map[string]any) (globalConditionGroups, error) { if injection == nil { return globalConditionGroups{}, nil } if err := unsupportedObjectKeysError("global injection", injection, globalInjectionKeys...); err != nil { return globalConditionGroups{}, err } var groups globalConditionGroups var err error if raw, ok := injection["require_present"]; ok { groups.RequirePresent, err = parseGlobalConditionList("require_present", raw, false) if err != nil { return globalConditionGroups{}, err } } if raw, ok := injection["any_present"]; ok { groups.AnyPresent, err = parseGlobalConditionList("any_present", raw, true) if err != nil { return globalConditionGroups{}, err } } if raw, ok := injection["unless_present"]; ok { groups.UnlessPresent, err = parseGlobalConditionList("unless_present", raw, false) if err != nil { return globalConditionGroups{}, err } } return groups, nil } func parseGlobalConditionList(name string, raw any, requireNonEmpty bool) ([]globalCondition, error) { if raw == nil { if requireNonEmpty { return nil, fmt.Errorf("%s must be an array", name) } return nil, nil } conditions, ok := raw.([]any) if !ok { return nil, fmt.Errorf("%s must be an array", name) } if requireNonEmpty && len(conditions) == 0 { return nil, fmt.Errorf("%s must contain at least one condition", name) } parsed := make([]globalCondition, 0, len(conditions)) for index, rawCondition := range conditions { condition, ok := rawCondition.(map[string]any) if !ok { return nil, fmt.Errorf("%s[%d] must be an object", name, index) } if err := unsupportedObjectKeysError(fmt.Sprintf("%s[%d]", name, index), condition, globalConditionKeys...); err != nil { return nil, err } field, _ := condition["field"].(string) field = strings.TrimSpace(field) if field == "" { return nil, fmt.Errorf("%s[%d].field is required", name, index) } id, _ := condition["id"].(string) id = strings.TrimSpace(id) if id == "" { return nil, fmt.Errorf("%s[%d].id is required", name, index) } parsed = append(parsed, globalCondition{Field: field, ID: id}) } return parsed, nil } func globalConditionGroupsMatch(groups globalConditionGroups, rows []map[string]any) bool { for _, condition := range groups.RequirePresent { if !globalReferencePresent(rows, condition.Field, condition.ID) { return false } } if groups.AnyPresent != nil { if len(groups.AnyPresent) == 0 { return false } matched := false for _, condition := range groups.AnyPresent { if globalReferencePresent(rows, condition.Field, condition.ID) { matched = true break } } if !matched { return false } } for _, condition := range groups.UnlessPresent { if globalReferencePresent(rows, condition.Field, condition.ID) { return false } } return true }