package topdata import ( "errors" "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, ", ")) } type globalConditionParseOptions struct { Label string Report func(error) } func parseGlobalConditionGroups(injection map[string]any, options ...globalConditionParseOptions) (globalConditionGroups, error) { if injection == nil { return globalConditionGroups{}, nil } label := "global injection" var report func(error) if len(options) > 0 { if strings.TrimSpace(options[0].Label) != "" { label = options[0].Label } report = options[0].Report } var errs []error addError := func(err error) { if err == nil { return } errs = append(errs, err) if report != nil { report(err) } } if err := unsupportedObjectKeysError(label, injection, globalInjectionKeys...); err != nil { addError(err) } var groups globalConditionGroups if raw, ok := injection["require_present"]; ok { parsed, err := parseGlobalConditionList(label+" require_present", raw, false) if err != nil { addError(err) } else { groups.RequirePresent = parsed } } if raw, ok := injection["any_present"]; ok { parsed, err := parseGlobalConditionList(label+" any_present", raw, true) if err != nil { addError(err) } else { groups.AnyPresent = parsed } } if raw, ok := injection["unless_present"]; ok { parsed, err := parseGlobalConditionList(label+" unless_present", raw, false) if err != nil { addError(err) } else { groups.UnlessPresent = parsed } } return groups, errors.Join(errs...) } func parseGlobalConditionList(name string, raw any, requireNonEmpty bool) ([]globalCondition, error) { if raw == nil { return nil, fmt.Errorf("%s must be an array", name) } 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 }