Reviewed-on: https://gitea.westgate.pw/ShadowsOverWestgate/sow-tools/pulls/8 Co-authored-by: vickydotbat <vickydotbat@tutamail.com> Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
346 lines
11 KiB
Go
346 lines
11 KiB
Go
package topdata
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
type familyIdentity struct {
|
|
Parent string
|
|
Child string
|
|
}
|
|
|
|
type familyExpansionSource struct {
|
|
Dataset string
|
|
Column string
|
|
Predicate string
|
|
}
|
|
|
|
type familyExpansionTitleStyle struct {
|
|
ChildCase string
|
|
ChildParenthetical 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
|
|
LegacyFamilyKeys []string
|
|
TitleStyle familyExpansionTitleStyle
|
|
}
|
|
|
|
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)
|
|
}
|
|
legacyFamilyKeys, err := parseOptionalStringArray(obj["legacy_family_keys"], "legacy_family_keys")
|
|
if err != nil {
|
|
return familyExpansionSpec{}, fmt.Errorf("generated file %s: %w", path, err)
|
|
}
|
|
if err := validateLegacyFamilyKeys(familyKey, legacyFamilyKeys); err != nil {
|
|
return familyExpansionSpec{}, fmt.Errorf("generated file %s: %w", path, err)
|
|
}
|
|
titleStyle, err := parseFamilyExpansionTitleStyle(obj["title_style"])
|
|
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,
|
|
LegacyFamilyKeys: legacyFamilyKeys,
|
|
TitleStyle: titleStyle,
|
|
}, nil
|
|
}
|
|
|
|
func parseFamilyExpansionTitleStyle(raw any) (familyExpansionTitleStyle, error) {
|
|
style := familyExpansionTitleStyle{
|
|
ChildCase: "preserve",
|
|
ChildParenthetical: "preserve",
|
|
}
|
|
if raw == nil {
|
|
return style, nil
|
|
}
|
|
obj, ok := raw.(map[string]any)
|
|
if !ok {
|
|
return familyExpansionTitleStyle{}, fmt.Errorf("title_style must be an object")
|
|
}
|
|
if childCase, ok := obj["child_case"].(string); ok && strings.TrimSpace(childCase) != "" {
|
|
style.ChildCase = strings.TrimSpace(childCase)
|
|
}
|
|
switch style.ChildCase {
|
|
case "preserve", "lower":
|
|
default:
|
|
return familyExpansionTitleStyle{}, fmt.Errorf("title_style.child_case must be preserve or lower")
|
|
}
|
|
if childParenthetical, ok := obj["child_parenthetical"].(string); ok && strings.TrimSpace(childParenthetical) != "" {
|
|
style.ChildParenthetical = strings.TrimSpace(childParenthetical)
|
|
}
|
|
switch style.ChildParenthetical {
|
|
case "preserve", "comma":
|
|
default:
|
|
return familyExpansionTitleStyle{}, fmt.Errorf("title_style.child_parenthetical must be preserve or comma")
|
|
}
|
|
return style, nil
|
|
}
|
|
|
|
func validateLegacyFamilyKeys(familyKey string, legacyFamilyKeys []string) error {
|
|
seen := map[string]string{}
|
|
normalizedFamilyKey := normalizeKeyIdentity(familyKey)
|
|
for _, legacyKey := range legacyFamilyKeys {
|
|
normalizedLegacyKey := normalizeKeyIdentity(legacyKey)
|
|
if normalizedLegacyKey == normalizedFamilyKey {
|
|
return fmt.Errorf("legacy_family_keys must not include family_key %q", familyKey)
|
|
}
|
|
if previous, ok := seen[normalizedLegacyKey]; ok {
|
|
return fmt.Errorf("legacy_family_keys contains duplicate-equivalent keys %q and %q", previous, legacyKey)
|
|
}
|
|
seen[normalizedLegacyKey] = legacyKey
|
|
}
|
|
return 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"]
|
|
_, hasLegacyFamilyKeys := obj["legacy_family_keys"]
|
|
_, hasTitleStyle := obj["title_style"]
|
|
return hasFamilyKey || hasTemplate || hasChildSource || hasNamePrefix || hasLabelPrefix ||
|
|
hasConstantPrefix || hasTemplateFields || hasDefaultFields || hasApplyAfterModules || hasChildRefField ||
|
|
hasIdentitySource || hasAllowExistingOnly || hasAutoPrereqFields || hasLegacyFamilyKeys || hasTitleStyle
|
|
}
|
|
|
|
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 ""
|
|
}
|