-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdiff.go
More file actions
154 lines (145 loc) · 4.4 KB
/
Copy pathdiff.go
File metadata and controls
154 lines (145 loc) · 4.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
package dyfields
import "sort"
// Diff produces the patch that turns base into next.
//
// It exists for the front end, and it is load-bearing rather than a
// convenience. A form that loaded a document, let the user change one field and
// posted the whole thing back would send every other field too -- including the
// masked ones, which it holds only as "******6789". Nothing downstream can tell
// that apart from a real value, so the only thing keeping a mask from
// overwriting what it hides is that an untouched field never leaves the client.
// Diff is how the client says only what it means.
//
// The result follows the patch rules exactly: a key that is absent means
// unchanged, an explicit null means clear, and object_list entries are aligned
// by $id, so an entry the user did not open contributes nothing and one the
// user deleted contributes {"$id": ..., "$deleted": true}.
//
// Apply(base, Diff(base, next)) reproduces next, with one deliberate
// exception: a secret that base holds and next does not is left alone rather
// than cleared. That follows the rule secrets already live by -- a form never
// reads a secret back, so "I do not have it" and "delete it" cannot be told
// apart from the values alone, and only one of the two is recoverable if the
// guess is wrong. Clearing a secret stays something the caller says outright.
func (c *Compiled) Diff(base, next ValueDocument) Patch {
p := Patch{Values: map[string]any{}}
diffScope(c.root, base.Values, next.Values, p.Values)
for k, nv := range next.Secrets {
if nv == "" || base.Secrets[k] == nv {
continue
}
if p.Secrets == nil {
p.Secrets = map[string]*string{}
}
v := nv
p.Secrets[k] = &v
}
return p
}
// diffScope walks the schema for the fields it knows and the two documents for
// the keys it does not, so a key that should not be there still reaches Apply
// and gets reported rather than quietly dropped on the way.
func diffScope(sc *cscope, base, next, out map[string]any) {
for _, k := range unionKeys(base, next) {
bv, bok := base[k]
nv, nok := next[k]
if !nok || nv == nil {
// Gone. Only worth saying if there was something there.
if bok && bv != nil {
out[k] = nil
}
continue
}
cf := sc.byName[k]
if !bok || bv == nil {
out[k] = cloneValue(nv)
continue
}
if equalValues(bv, nv) {
continue
}
if cf != nil && cf.f.Type == TypeObjectList {
if entries := diffObjectList(cf, bv, nv); len(entries) > 0 {
out[k] = entries
}
continue
}
// list and map are replaced wholesale, because that is how Apply
// merges them. Diffing them element by element would produce a patch
// the other half of the library cannot read.
out[k] = cloneValue(nv)
}
}
// diffObjectList reports the entries that changed, were added, or are gone.
// Entries are matched by $id and nothing else: position is not identity here,
// which is the same reason the merge side aligns by $id.
func diffObjectList(cf *cfield, baseRaw, nextRaw any) []any {
nextArr, ok := nextRaw.([]any)
if !ok {
return nil
}
baseArr, _ := baseRaw.([]any)
baseByID := map[string]map[string]any{}
baseOrder := make([]string, 0, len(baseArr))
for _, e := range baseArr {
item, ok := e.(map[string]any)
if !ok {
continue
}
if id, _ := item[KeyItemID].(string); id != "" {
if _, dup := baseByID[id]; !dup {
baseOrder = append(baseOrder, id)
}
baseByID[id] = item
}
}
out := []any{}
seen := map[string]bool{}
for _, e := range nextArr {
item, ok := e.(map[string]any)
if !ok {
out = append(out, cloneValue(e))
continue
}
id, _ := item[KeyItemID].(string)
if id == "" {
// No $id means the client just created it, so all of it is new.
out = append(out, cloneValue(item))
continue
}
seen[id] = true
prev, found := baseByID[id]
if !found {
out = append(out, cloneValue(item))
continue
}
sub := map[string]any{}
diffScope(cf.child, prev, item, sub)
if len(sub) == 0 {
continue
}
sub[KeyItemID] = id
out = append(out, sub)
}
for _, id := range baseOrder {
if !seen[id] {
out = append(out, map[string]any{KeyItemID: id, KeyDeleted: true})
}
}
return out
}
func unionKeys(a, b map[string]any) []string {
set := make(map[string]struct{}, len(a)+len(b))
for k := range a {
set[k] = struct{}{}
}
for k := range b {
set[k] = struct{}{}
}
keys := make([]string, 0, len(set))
for k := range set {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}