package engine import ( "cmp" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" "slices" ) type documentDeltaKind uint8 const ( documentAdded documentDeltaKind = iota + 1 documentRemoved documentReplaced ) type documentDelta struct { path Path kind documentDeltaKind } // compareConfigurationValues ignores locations and sensitivity metadata. Mapping // order is immaterial; sequences are atomic for change reporting. func compareConfigurationValues(before, after *document.Node) []documentDelta { var deltas []documentDelta compareConfigurationValue(before, after, Path{}, &deltas) slices.SortFunc(deltas, func(left, right documentDelta) int { if order := cmp.Compare(left.path.String(), right.path.String()); order != 0 { return order } return int(left.kind) - int(right.kind) }) return deltas } func compareConfigurationValue(before, after *document.Node, path Path, deltas *[]documentDelta) { if before == nil && after == nil { return } if before == nil { *deltas = append(*deltas, documentDelta{path: path.clone(), kind: documentAdded}) return } if after == nil { *deltas = append(*deltas, documentDelta{path: path.clone(), kind: documentRemoved}) return } if before.Kind != after.Kind { *deltas = append(*deltas, documentDelta{path: path.clone(), kind: documentReplaced}) return } switch before.Kind { case document.Mapping: beforeEntries := make(map[string]*document.Node, len(before.Mapping)) afterEntries := make(map[string]*document.Node, len(after.Mapping)) keys := make(map[string]bool, len(before.Mapping)+len(after.Mapping)) for _, entry := range before.Mapping { beforeEntries[entry.Key] = entry.Value keys[entry.Key] = true } for _, entry := range after.Mapping { afterEntries[entry.Key] = entry.Value keys[entry.Key] = true } ordered := make([]string, 0, len(keys)) for key := range keys { ordered = append(ordered, key) } slices.Sort(ordered) for _, key := range ordered { compareConfigurationValue(beforeEntries[key], afterEntries[key], path.Key(key), deltas) } case document.Sequence: if !configurationValuesEqual(before, after) { *deltas = append(*deltas, documentDelta{path: path.clone(), kind: documentReplaced}) } default: if before.Text != after.Text { *deltas = append(*deltas, documentDelta{path: path.clone(), kind: documentReplaced}) } } } // configurationValuesEqual compares only configuration values, not provenance or // sensitivity. Callers auditing validation must check sensitivity separately. func configurationValuesEqual(left, right *document.Node) bool { if left == nil || right == nil { return left == right } if left.Kind != right.Kind || left.Text != right.Text || len(left.Mapping) != len(right.Mapping) || len(left.Sequence) != len(right.Sequence) { return false } if left.Kind == document.Mapping { leftMap := make(map[string]*document.Node, len(left.Mapping)) for _, entry := range left.Mapping { leftMap[entry.Key] = entry.Value } for _, entry := range right.Mapping { if !configurationValuesEqual(leftMap[entry.Key], entry.Value) { return false } } } for i := range left.Sequence { if !configurationValuesEqual(left.Sequence[i], right.Sequence[i]) { return false } } return true }