package topdata import ( "fmt" "strings" ) type familyIdentity struct { Parent string Child string } type familyExpansionSource struct { Dataset string Column string Predicate string } type familyExpansionSpec struct { Family string FamilyKey string Template string ChildSource familyExpansionSource NamePrefix string LabelPrefix string ConstantPrefix string TemplateFields []string DefaultFields map[string]any ApplyAfterModules bool ChildRefField string IdentitySource string AllowExistingOnly bool AutoPrereqFields map[string]string } func splitFamilyExpansionIdentity(text string) familyIdentity { text = strings.TrimSpace(text) if text == "" { return familyIdentity{} } if idx := strings.Index(text, "_"); idx > 0 && idx < len(text)-1 { return familyIdentity{ Parent: text[:idx], Child: text[idx+1:], } } return familyIdentity{Parent: text} } func parseFamilyExpansionSource(raw any) (familyExpansionSource, error) { if raw == nil { return familyExpansionSource{}, fmt.Errorf("child_source is required") } obj, ok := raw.(map[string]any) if !ok { return familyExpansionSource{}, fmt.Errorf("child_source must be an object") } dataset, ok := obj["dataset"].(string) if !ok || strings.TrimSpace(dataset) == "" { return familyExpansionSource{}, fmt.Errorf("child_source.dataset must be a non-empty string") } source := familyExpansionSource{ Dataset: strings.TrimSpace(dataset), } if column, ok := obj["column"].(string); ok && strings.TrimSpace(column) != "" { source.Column = strings.TrimSpace(column) } if predicate, ok := obj["predicate"].(string); ok && strings.TrimSpace(predicate) != "" { source.Predicate = strings.TrimSpace(predicate) } return source, nil } func parseFamilyExpansionSpec(path string, obj map[string]any) (familyExpansionSpec, error) { family, ok := obj["family"].(string) if !ok || strings.TrimSpace(family) == "" { return familyExpansionSpec{}, fmt.Errorf("generated file %s: family must be a non-empty string", path) } familyKey, ok := obj["family_key"].(string) if !ok || strings.TrimSpace(familyKey) == "" { return familyExpansionSpec{}, fmt.Errorf("generated file %s: family_key must be a non-empty string", path) } template, ok := obj["template"].(string) if !ok || strings.TrimSpace(template) == "" { return familyExpansionSpec{}, fmt.Errorf("generated file %s: template must be a non-empty string", path) } source, err := parseFamilyExpansionSource(obj["child_source"]) if err != nil { return familyExpansionSpec{}, fmt.Errorf("generated file %s: %w", path, err) } namePrefix, _ := optionalTrimmedString(obj, "name_prefix") labelPrefix, _ := optionalTrimmedString(obj, "label_prefix") constantPrefix, _ := optionalTrimmedString(obj, "constant_prefix") templateFields, err := parseOptionalStringArray(obj["template_fields"], "template_fields") if err != nil { return familyExpansionSpec{}, fmt.Errorf("generated file %s: %w", path, err) } defaultFields, err := parseOptionalObject(obj["default_fields"], "default_fields") if err != nil { return familyExpansionSpec{}, fmt.Errorf("generated file %s: %w", path, err) } applyAfterModules, err := parseOptionalBoolField(obj["apply_after_modules"], "apply_after_modules") if err != nil { return familyExpansionSpec{}, fmt.Errorf("generated file %s: %w", path, err) } childRefField, _ := optionalTrimmedString(obj, "child_ref_field") identitySource, _ := optionalTrimmedString(obj, "identity_source") switch identitySource { case "", "child_source_value": default: return familyExpansionSpec{}, fmt.Errorf("generated file %s: identity_source must be empty or child_source_value", path) } autoPrereqFields, err := parseOptionalStringMap(obj["auto_prereq_fields"], "auto_prereq_fields") if err != nil { return familyExpansionSpec{}, fmt.Errorf("generated file %s: %w", path, err) } allowExistingOnly, err := parseOptionalBoolField(obj["allow_existing_only"], "allow_existing_only") if err != nil { return familyExpansionSpec{}, fmt.Errorf("generated file %s: %w", path, err) } return familyExpansionSpec{ Family: strings.TrimSpace(family), FamilyKey: strings.TrimSpace(familyKey), Template: strings.TrimSpace(template), ChildSource: source, NamePrefix: namePrefix, LabelPrefix: labelPrefix, ConstantPrefix: constantPrefix, TemplateFields: templateFields, DefaultFields: defaultFields, ApplyAfterModules: applyAfterModules, ChildRefField: childRefField, IdentitySource: identitySource, AllowExistingOnly: allowExistingOnly, AutoPrereqFields: autoPrereqFields, }, nil } func isFamilyExpansionObject(obj map[string]any) bool { _, hasFamilyKey := obj["family_key"] _, hasTemplate := obj["template"] _, hasChildSource := obj["child_source"] _, hasNamePrefix := obj["name_prefix"] _, hasLabelPrefix := obj["label_prefix"] _, hasConstantPrefix := obj["constant_prefix"] _, hasTemplateFields := obj["template_fields"] _, hasDefaultFields := obj["default_fields"] _, hasApplyAfterModules := obj["apply_after_modules"] _, hasChildRefField := obj["child_ref_field"] _, hasIdentitySource := obj["identity_source"] _, hasAllowExistingOnly := obj["allow_existing_only"] _, hasAutoPrereqFields := obj["auto_prereq_fields"] return hasFamilyKey || hasTemplate || hasChildSource || hasNamePrefix || hasLabelPrefix || hasConstantPrefix || hasTemplateFields || hasDefaultFields || hasApplyAfterModules || hasChildRefField || hasIdentitySource || hasAllowExistingOnly || hasAutoPrereqFields } func optionalTrimmedString(obj map[string]any, field string) (string, bool) { text, ok := obj[field].(string) if !ok || strings.TrimSpace(text) == "" { return "", false } return strings.TrimSpace(text), true } func parseOptionalStringArray(raw any, field string) ([]string, error) { if raw == nil { return nil, nil } items, ok := raw.([]any) if !ok { return nil, fmt.Errorf("%s must be an array of strings", field) } out := make([]string, 0, len(items)) for _, item := range items { text, ok := item.(string) if !ok || strings.TrimSpace(text) == "" { return nil, fmt.Errorf("%s must contain only non-empty strings", field) } out = append(out, strings.TrimSpace(text)) } return out, nil } func parseOptionalObject(raw any, field string) (map[string]any, error) { if raw == nil { return nil, nil } obj, ok := raw.(map[string]any) if !ok { return nil, fmt.Errorf("%s must be an object", field) } return obj, nil } func parseOptionalStringMap(raw any, field string) (map[string]string, error) { if raw == nil { return nil, nil } obj, ok := raw.(map[string]any) if !ok { return nil, fmt.Errorf("%s must be an object of strings", field) } out := make(map[string]string, len(obj)) for key, value := range obj { text, ok := value.(string) if !ok || strings.TrimSpace(text) == "" { return nil, fmt.Errorf("%s must contain only non-empty string values", field) } out[key] = strings.TrimSpace(text) } return out, nil } func parseOptionalBoolField(raw any, field string) (bool, error) { if raw == nil { return false, nil } value, ok := raw.(bool) if !ok { return false, fmt.Errorf("%s must be a boolean", field) } return value, nil } func familyMetadata(parent, child, source, template string) map[string]any { meta := map[string]any{ "parent": strings.TrimSpace(parent), } if strings.TrimSpace(child) != "" { meta["child"] = strings.TrimSpace(child) } if strings.TrimSpace(source) != "" { meta["source"] = strings.TrimSpace(source) } if strings.TrimSpace(template) != "" { meta["template"] = strings.TrimSpace(template) } return map[string]any{"family": meta} } func parseFamilyMetadata(raw any) (map[string]any, error) { obj, ok := raw.(map[string]any) if !ok { return nil, fmt.Errorf("must be an object") } parent, ok := obj["parent"].(string) if !ok || strings.TrimSpace(parent) == "" { return nil, fmt.Errorf("parent must be a non-empty string") } meta := map[string]any{ "parent": strings.TrimSpace(parent), } if child, ok := obj["child"].(string); ok && strings.TrimSpace(child) != "" { meta["child"] = strings.TrimSpace(child) } if source, ok := obj["source"].(string); ok && strings.TrimSpace(source) != "" { meta["source"] = strings.TrimSpace(source) } if template, ok := obj["template"].(string); ok && strings.TrimSpace(template) != "" { meta["template"] = strings.TrimSpace(template) } return meta, nil } func childTokenFromExpandedKey(key, parent string) string { key = strings.TrimSpace(key) key = strings.TrimPrefix(key, "feat:") if strings.HasPrefix(key, parent+"_") { return strings.TrimPrefix(key, parent+"_") } if strings.HasPrefix(key, parent) { return strings.TrimPrefix(key, parent) } return "" }