package consumers import ( "fmt" "strconv" "strings" configkit "gitea.maximumdirect.net/eric/ConfigKit" ) // FileComparisonConfig is a small application-owned shape for comparing // caller-selected configuration files. type FileComparisonConfig struct { Name string `yaml:"name"` Entries map[string]any `yaml:"entries"` } // CompareFiles loads two caller-selected files with the same application // definition, then formats only the safe records returned by configkit.Diff. func CompareFiles(leftPath, rightPath string) (string, error) { options := configkit.Options[FileComparisonConfig]{ Definition: configkit.Definition[FileComparisonConfig]{ Defaults: func() FileComparisonConfig { return FileComparisonConfig{Entries: map[string]any{}} }, }, } left, err := configkit.Load(configkit.File(leftPath), options) if err != nil { return "", fmt.Errorf("load left configuration: %w", err) } right, err := configkit.Load(configkit.File(rightPath), options) if err != nil { return "", fmt.Errorf("load right configuration: %w", err) } differences, err := configkit.Diff(left, right) if err != nil { return "", err } return FormatDifferences(differences), nil } // RolloutConfig models a service endpoint whose URL is derived by the // application resolver and whose credential reference is classified. type RolloutConfig struct { Service RolloutService `yaml:"service"` } // RolloutService contains endpoint settings and a classified credential. type RolloutService struct { Endpoint RolloutEndpoint `yaml:"endpoint"` Credential RolloutCredential `yaml:"credential"` } // RolloutEndpoint is the effective destination used by the application. type RolloutEndpoint struct { Host string `yaml:"host"` Port int `yaml:"port"` URL string `yaml:"url"` } // RolloutCredential holds only a typed secret reference in configuration. type RolloutCredential struct { Token configkit.Secret `yaml:"token" configkit:"secret"` } // LoadRollout loads a base file, optionally with one selected configuration // overlay. Resolution derives the effective URL after the overlay is applied. func LoadRollout(basePath, overlayName string) (*configkit.Result[RolloutConfig], error) { return configkit.Load(configkit.File(basePath), configkit.Options[RolloutConfig]{ Overlay: overlayName, Definition: configkit.Definition[RolloutConfig]{ Resolve: resolveRollout, }, }) } // CompareRolloutOverlay compares the effective base configuration with that // same file plus one selected overlay. It presents only safe diff records. func CompareRolloutOverlay(basePath, overlayName string) (string, error) { base, err := LoadRollout(basePath, "") if err != nil { return "", fmt.Errorf("load base configuration: %w", err) } withOverlay, err := LoadRollout(basePath, overlayName) if err != nil { return "", fmt.Errorf("load overlay configuration: %w", err) } differences, err := configkit.Diff(base, withOverlay) if err != nil { return "", err } return FormatDifferences(differences), nil } func resolveRollout(configuration *RolloutConfig, resolution *configkit.Resolution) error { endpoint := &configuration.Service.Endpoint if endpoint.Host == "" || endpoint.Port <= 0 { return nil } url := fmt.Sprintf("https://%s:%d", endpoint.Host, endpoint.Port) if endpoint.URL != url { endpoint.URL = url resolution.Record(configkit.Keys("service", "endpoint", "url"), configkit.ChangeNormalization, "derived endpoint URL", configkit.Keys("service", "endpoint", "host"), configkit.Keys("service", "endpoint", "port")) } return nil } // FormatDifferences is consumer-owned presentation. It distinguishes absence // through Present and reports withheld changes through HiddenChanged without // interpreting redaction marker text. func FormatDifferences(differences []configkit.Difference) string { if len(differences) == 0 { return "No effective changes.\n" } var output strings.Builder for _, difference := range differences { fmt.Fprintf(&output, "%s: left=%s right=%s", difference.Path.String(), formatSnapshot(difference.Left), formatSnapshot(difference.Right)) if difference.HiddenChanged { output.WriteString(" hidden-change") } output.WriteByte('\n') } return output.String() } func formatSnapshot(snapshot configkit.ValueSnapshot) string { if !snapshot.Present { return "" } return strconv.Quote(snapshot.YAML) }