cuelang.org/go@v0.13.0/internal/diff/diff.go (about) 1 // Copyright 2019 CUE Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package diff 16 17 import ( 18 "cuelang.org/go/cue" 19 ) 20 21 // Profile configures a diff operation. 22 type Profile struct { 23 Concrete bool 24 25 // Hidden fields are only useful to compare when a values are from the same 26 // package. 27 SkipHidden bool 28 29 // TODO: Use this method instead of SkipHidden. To do this, we need to have 30 // access the package associated with a hidden field, which is only 31 // accessible through the Iterator API. And we should probably get rid of 32 // the cue.Struct API. 33 // 34 // HiddenPkg compares hidden fields for the package if this is not the empty 35 // string. Use "_" for the anonymous package. 36 // HiddenPkg string 37 } 38 39 var ( 40 // Schema is the standard profile used for comparing schema. 41 Schema = &Profile{} 42 43 // Final is the standard profile for comparing data. 44 Final = &Profile{ 45 Concrete: true, 46 } 47 ) 48 49 // TODO: don't return Kind, which is always Modified or not. 50 51 // Diff is a shorthand for Schema.Diff. 52 func Diff(x, y cue.Value) (Kind, *EditScript) { 53 return Schema.Diff(x, y) 54 } 55 56 // Diff returns an edit script representing the difference between x and y. 57 func (p *Profile) Diff(x, y cue.Value) (Kind, *EditScript) { 58 d := differ{cfg: *p} 59 k, es := d.diffValue(x, y) 60 if k == Modified && es == nil { 61 es = &EditScript{X: x, Y: y} 62 } 63 return k, es 64 } 65 66 // Kind identifies the kind of operation of an edit script. 67 type Kind uint8 68 69 const ( 70 // Identity indicates that a value pair is identical in both list X and Y. 71 Identity Kind = iota 72 // UniqueX indicates that a value only exists in X and not Y. 73 UniqueX 74 // UniqueY indicates that a value only exists in Y and not X. 75 UniqueY 76 // Modified indicates that a value pair is a modification of each other. 77 Modified 78 ) 79 80 // EditScript represents the series of differences between two CUE values. 81 // x and y must be either both list or struct. 82 type EditScript struct { 83 X, Y cue.Value 84 Edits []Edit 85 } 86 87 // Edit represents a single operation within an edit-script. 88 type Edit struct { 89 Kind Kind 90 XSel cue.Selector // valid if UniqueY 91 YSel cue.Selector // valid if UniqueX 92 Sub *EditScript // non-nil if Modified 93 } 94 95 type differ struct { 96 cfg Profile 97 } 98 99 func (d *differ) diffValue(x, y cue.Value) (Kind, *EditScript) { 100 if d.cfg.Concrete { 101 x, _ = x.Default() 102 y, _ = y.Default() 103 } 104 if x.IncompleteKind() != y.IncompleteKind() { 105 return Modified, nil 106 } 107 108 switch xc, yc := x.IsConcrete(), y.IsConcrete(); { 109 case xc != yc: 110 return Modified, nil 111 112 case xc && yc: 113 switch k := x.Kind(); k { 114 case cue.StructKind: 115 return d.diffStruct(x, y) 116 117 case cue.ListKind: 118 return d.diffList(x, y) 119 } 120 fallthrough 121 122 default: 123 // In concrete mode we do not care about non-concrete values. 124 if d.cfg.Concrete { 125 return Identity, nil 126 } 127 128 if !x.Equals(y) { 129 return Modified, nil 130 } 131 } 132 133 return Identity, nil 134 } 135 136 type field struct { 137 sel cue.Selector 138 val cue.Value 139 } 140 141 // TODO(mvdan): use slices.Collect once we swap cue.Iterator for a Go iterator 142 func (d *differ) collectFields(v cue.Value) []field { 143 iter, _ := v.Fields(cue.Hidden(!d.cfg.SkipHidden), cue.Definitions(true), cue.Optional(true)) 144 var fields []field 145 for iter.Next() { 146 fields = append(fields, field{iter.Selector(), iter.Value()}) 147 } 148 return fields 149 } 150 151 func (d *differ) diffStruct(x, y cue.Value) (Kind, *EditScript) { 152 xFields := d.collectFields(x) 153 yFields := d.collectFields(y) 154 155 // Best-effort topological sort, prioritizing x over y, using a variant of 156 // Kahn's algorithm (see, for instance 157 // https://www.geeksforgeeks.org/topological-sorting-indegree-based-solution/). 158 // We assume that the order of the elements of each value indicate an edge 159 // in the graph. This means that only the next unprocessed nodes can be 160 // those with no incoming edges. 161 xMap := make(map[cue.Selector]struct{}, len(xFields)) 162 yMap := make(map[cue.Selector]int, len(yFields)) 163 for _, f := range xFields { 164 xMap[f.sel] = struct{}{} 165 } 166 for i, f := range yFields { 167 yMap[f.sel] = i + 1 168 } 169 170 edits := []Edit{} 171 differs := false 172 173 for xi, yi := 0, 0; xi < len(xFields) || yi < len(yFields); { 174 // Process zero nodes 175 for ; xi < len(xFields); xi++ { 176 xf := xFields[xi] 177 yp := yMap[xf.sel] 178 if yp > 0 { 179 break 180 } 181 edits = append(edits, Edit{UniqueX, xf.sel, cue.Selector{}, nil}) 182 differs = true 183 } 184 for ; yi < len(yFields); yi++ { 185 yf := yFields[yi] 186 if yMap[yf.sel] == 0 { 187 // already done 188 continue 189 } 190 if _, ok := xMap[yf.sel]; ok { 191 break 192 } 193 yMap[yf.sel] = 0 194 edits = append(edits, Edit{UniqueY, cue.Selector{}, yf.sel, nil}) 195 differs = true 196 } 197 198 // Compare nodes 199 for ; xi < len(xFields); xi++ { 200 xf := xFields[xi] 201 yp := yMap[xf.sel] 202 if yp == 0 { 203 break 204 } 205 // If yp != xi+1, the topological sort was not possible. 206 yMap[xf.sel] = 0 207 208 yf := yFields[yp-1] 209 210 var kind Kind 211 var script *EditScript 212 switch { 213 case xf.sel.IsDefinition() != yf.sel.IsDefinition(), xf.sel.ConstraintType() != yf.sel.ConstraintType(): 214 kind = Modified 215 default: 216 // TODO(perf): consider evaluating lazily. 217 kind, script = d.diffValue(xf.val, yf.val) 218 } 219 220 edits = append(edits, Edit{kind, xf.sel, yf.sel, script}) 221 differs = differs || kind != Identity 222 } 223 } 224 if !differs { 225 return Identity, nil 226 } 227 return Modified, &EditScript{X: x, Y: y, Edits: edits} 228 } 229 230 // TODO: right now we do a simple element-by-element comparison. Instead, 231 // use an algorithm that approximates a minimal Levenshtein distance, like the 232 // one in github.com/google/go-cmp/internal/diff. 233 func (d *differ) diffList(x, y cue.Value) (Kind, *EditScript) { 234 ix, _ := x.List() 235 iy, _ := y.List() 236 237 edits := []Edit{} 238 differs := false 239 i := 0 240 241 for { 242 // TODO: This would be much easier with a Next/Done API. 243 hasX := ix.Next() 244 hasY := iy.Next() 245 if !hasX { 246 for hasY { 247 differs = true 248 edits = append(edits, Edit{UniqueY, cue.Selector{}, cue.Index(i), nil}) 249 hasY = iy.Next() 250 i++ 251 } 252 break 253 } 254 if !hasY { 255 for hasX { 256 differs = true 257 edits = append(edits, Edit{UniqueX, cue.Index(i), cue.Selector{}, nil}) 258 hasX = ix.Next() 259 i++ 260 } 261 break 262 } 263 264 // Both x and y have a value. 265 kind, script := d.diffValue(ix.Value(), iy.Value()) 266 if kind != Identity { 267 differs = true 268 } 269 edits = append(edits, Edit{kind, cue.Index(i), cue.Index(i), script}) 270 i++ 271 } 272 if !differs { 273 return Identity, nil 274 } 275 return Modified, &EditScript{X: x, Y: y, Edits: edits} 276 }