package confmgr import ( "fmt" "sort" ) // ChangeStatus classifies a single diff entry. type ChangeStatus string const ( StatusAdded ChangeStatus = "added" StatusRemoved ChangeStatus = "removed" StatusChanged ChangeStatus = "changed" StatusUnchanged ChangeStatus = "unchanged" ) // SetDiffEntry is one parameter-level difference between two config sets. type SetDiffEntry struct { Key string `json:"key"` Status ChangeStatus `json:"status"` Left *Parameter `json:"left,omitempty"` Right *Parameter `json:"right,omitempty"` } // InstanceDiffEntry is one value-level difference between two config instances. type InstanceDiffEntry struct { Key string `json:"key"` Status ChangeStatus `json:"status"` Left any `json:"left,omitempty"` Right any `json:"right,omitempty"` } // DiffSets compares two config sets parameter-by-parameter (keyed by Key), // returning entries sorted by key. Both sides are included so the frontend can // render either unified or side-by-side. func DiffSets(left, right ConfigSet) []SetDiffEntry { keys := map[string]bool{} li := map[string]Parameter{} ri := map[string]Parameter{} for _, p := range left.Parameters { li[p.Key] = p keys[p.Key] = true } for _, p := range right.Parameters { ri[p.Key] = p keys[p.Key] = true } out := make([]SetDiffEntry, 0, len(keys)) for k := range keys { l, lok := li[k] r, rok := ri[k] e := SetDiffEntry{Key: k} switch { case lok && !rok: e.Status = StatusRemoved lp := l e.Left = &lp case !lok && rok: e.Status = StatusAdded rp := r e.Right = &rp default: lp, rp := l, r e.Left, e.Right = &lp, &rp if paramEqual(l, r) { e.Status = StatusUnchanged } else { e.Status = StatusChanged } } out = append(out, e) } sort.Slice(out, func(i, j int) bool { return out[i].Key < out[j].Key }) return out } // DiffInstances compares two config instances value-by-value (keyed by // parameter key), returning entries sorted by key. func DiffInstances(left, right ConfigInstance) []InstanceDiffEntry { keys := map[string]bool{} for k := range left.Values { keys[k] = true } for k := range right.Values { keys[k] = true } out := make([]InstanceDiffEntry, 0, len(keys)) for k := range keys { lv, lok := left.Values[k] rv, rok := right.Values[k] e := InstanceDiffEntry{Key: k, Left: lv, Right: rv} switch { case lok && !rok: e.Status = StatusRemoved case !lok && rok: e.Status = StatusAdded case valueEqual(lv, rv): e.Status = StatusUnchanged default: e.Status = StatusChanged } out = append(out, e) } sort.Slice(out, func(i, j int) bool { return out[i].Key < out[j].Key }) return out } func paramEqual(a, b Parameter) bool { if a.Label != b.Label || a.Group != b.Group || a.Subgroup != b.Subgroup || a.DS != b.DS || a.Signal != b.Signal || a.Type != b.Type || a.Mandatory != b.Mandatory || a.Unit != b.Unit || a.Description != b.Description { return false } if !valueEqual(a.Default, b.Default) || !floatPtrEqual(a.Min, b.Min) || !floatPtrEqual(a.Max, b.Max) { return false } if len(a.EnumValues) != len(b.EnumValues) { return false } for i := range a.EnumValues { if a.EnumValues[i] != b.EnumValues[i] { return false } } return true } func floatPtrEqual(a, b *float64) bool { if a == nil || b == nil { return a == b } return *a == *b } // valueEqual compares two JSON-decoded scalar values for diff purposes by // rendering them to a canonical string. func valueEqual(a, b any) bool { return fmt.Sprintf("%v", a) == fmt.Sprintf("%v", b) }