github.com/solo-io/cue@v0.4.7/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 "github.com/solo-io/cue/cue" 19 "github.com/solo-io/cue/cue/errors" 20 ) 21 22 // Profile configures a diff operation. 23 type Profile struct { 24 Concrete bool 25 } 26 27 var ( 28 // Schema is the standard profile used for comparing schema. 29 Schema = &Profile{} 30 31 // Final is the standard profile for comparing data. 32 Final = &Profile{ 33 Concrete: true, 34 } 35 ) 36 37 // TODO: don't return Kind, which is always Modified or not. 38 39 // Diff is a shorthand for Schema.Diff. 40 func Diff(x, y cue.Value) (Kind, *EditScript) { 41 return Schema.Diff(x, y) 42 } 43 44 // Diff returns an edit script representing the difference between x and y. 45 func (p *Profile) Diff(x, y cue.Value) (Kind, *EditScript) { 46 d := differ{cfg: *p} 47 k, es := d.diffValue(x, y) 48 if k == Modified && es == nil { 49 es = &EditScript{x: x, y: y} 50 } 51 return k, es 52 } 53 54 // Kind identifies the kind of operation of an edit script. 55 type Kind uint8 56 57 const ( 58 // Identity indicates that a value pair is identical in both list X and Y. 59 Identity Kind = iota 60 // UniqueX indicates that a value only exists in X and not Y. 61 UniqueX 62 // UniqueY indicates that a value only exists in Y and not X. 63 UniqueY 64 // Modified indicates that a value pair is a modification of each other. 65 Modified 66 ) 67 68 // EditScript represents the series of differences between two CUE values. 69 // x and y must be either both list or struct. 70 type EditScript struct { 71 x, y cue.Value 72 edits []Edit 73 } 74 75 // Len returns the number of edits. 76 func (es *EditScript) Len() int { 77 return len(es.edits) 78 } 79 80 // Label returns a string representation of the label. 81 // 82 func (es *EditScript) LabelX(i int) string { 83 e := es.edits[i] 84 p := e.XPos() 85 if p < 0 { 86 return "" 87 } 88 return label(es.x, p) 89 } 90 91 func (es *EditScript) LabelY(i int) string { 92 e := es.edits[i] 93 p := e.YPos() 94 if p < 0 { 95 return "" 96 } 97 return label(es.y, p) 98 } 99 100 // TODO: support label expressions. 101 func label(v cue.Value, i int) string { 102 st, err := v.Struct() 103 if err != nil { 104 return "" 105 } 106 107 // TODO: return formatted expression for optionals. 108 f := st.Field(i) 109 str := f.Selector 110 if f.IsOptional { 111 str += "?" 112 } 113 str += ":" 114 return str 115 } 116 117 // ValueX returns the value of X involved at step i. 118 func (es *EditScript) ValueX(i int) (v cue.Value) { 119 p := es.edits[i].XPos() 120 if p < 0 { 121 return v 122 } 123 st, err := es.x.Struct() 124 if err != nil { 125 return v 126 } 127 return st.Field(p).Value 128 } 129 130 // ValueY returns the value of Y involved at step i. 131 func (es *EditScript) ValueY(i int) (v cue.Value) { 132 p := es.edits[i].YPos() 133 if p < 0 { 134 return v 135 } 136 st, err := es.y.Struct() 137 if err != nil { 138 return v 139 } 140 return st.Field(p).Value 141 } 142 143 // Edit represents a single operation within an edit-script. 144 type Edit struct { 145 kind Kind 146 xPos int32 // 0 if UniqueY 147 yPos int32 // 0 if UniqueX 148 sub *EditScript // non-nil if Modified 149 } 150 151 func (e Edit) Kind() Kind { return e.kind } 152 func (e Edit) XPos() int { return int(e.xPos - 1) } 153 func (e Edit) YPos() int { return int(e.yPos - 1) } 154 155 type differ struct { 156 cfg Profile 157 options []cue.Option 158 errs errors.Error 159 } 160 161 func (d *differ) diffValue(x, y cue.Value) (Kind, *EditScript) { 162 if d.cfg.Concrete { 163 x, _ = x.Default() 164 y, _ = y.Default() 165 } 166 if x.IncompleteKind() != y.IncompleteKind() { 167 return Modified, nil 168 } 169 170 switch xc, yc := x.IsConcrete(), y.IsConcrete(); { 171 case xc != yc: 172 return Modified, nil 173 174 case xc && yc: 175 switch k := x.Kind(); k { 176 case cue.StructKind: 177 return d.diffStruct(x, y) 178 179 case cue.ListKind: 180 return d.diffList(x, y) 181 } 182 fallthrough 183 184 default: 185 // In concrete mode we do not care about non-concrete values. 186 if d.cfg.Concrete { 187 return Identity, nil 188 } 189 190 if !x.Equals(y) { 191 return Modified, nil 192 } 193 } 194 195 return Identity, nil 196 } 197 198 func (d *differ) diffStruct(x, y cue.Value) (Kind, *EditScript) { 199 sx, _ := x.Struct() 200 sy, _ := y.Struct() 201 202 // Best-effort topological sort, prioritizing x over y, using a variant of 203 // Kahn's algorithm (see, for instance 204 // https://www.geeksforgeeks.org/topological-sorting-indegree-based-solution/). 205 // We assume that the order of the elements of each value indicate an edge 206 // in the graph. This means that only the next unprocessed nodes can be 207 // those with no incoming edges. 208 xMap := make(map[string]int32, sx.Len()) 209 yMap := make(map[string]int32, sy.Len()) 210 for i := 0; i < sx.Len(); i++ { 211 xMap[sx.Field(i).Selector] = int32(i + 1) 212 } 213 for i := 0; i < sy.Len(); i++ { 214 yMap[sy.Field(i).Selector] = int32(i + 1) 215 } 216 217 edits := []Edit{} 218 differs := false 219 220 var xi, yi int 221 var xf, yf cue.FieldInfo 222 for xi < sx.Len() || yi < sy.Len() { 223 // Process zero nodes 224 for ; xi < sx.Len(); xi++ { 225 xf = sx.Field(xi) 226 yp := yMap[xf.Selector] 227 if yp > 0 { 228 break 229 } 230 edits = append(edits, Edit{UniqueX, int32(xi + 1), 0, nil}) 231 differs = true 232 } 233 for ; yi < sy.Len(); yi++ { 234 yf = sy.Field(yi) 235 if yMap[yf.Selector] == 0 { 236 // already done 237 continue 238 } 239 xp := xMap[yf.Selector] 240 if xp > 0 { 241 break 242 } 243 yMap[yf.Selector] = 0 244 edits = append(edits, Edit{UniqueY, 0, int32(yi + 1), nil}) 245 differs = true 246 } 247 248 // Compare nodes 249 for ; xi < sx.Len(); xi++ { 250 xf = sx.Field(xi) 251 252 yp := yMap[xf.Selector] 253 if yp == 0 { 254 break 255 } 256 // If yp != xi+1, the topological sort was not possible. 257 yMap[xf.Selector] = 0 258 259 yf := sy.Field(int(yp - 1)) 260 261 kind := Identity 262 var script *EditScript 263 switch { 264 case xf.IsDefinition != yf.IsDefinition, 265 xf.IsOptional != yf.IsOptional: 266 kind = Modified 267 268 default: 269 xv := xf.Value 270 yv := yf.Value 271 // TODO(perf): consider evaluating lazily. 272 kind, script = d.diffValue(xv, yv) 273 } 274 275 edits = append(edits, Edit{kind, int32(xi + 1), yp, script}) 276 if kind != Identity { 277 differs = true 278 } 279 } 280 } 281 if !differs { 282 return Identity, nil 283 } 284 return Modified, &EditScript{x: x, y: y, edits: edits} 285 } 286 287 // TODO: right now we do a simple element-by-element comparison. Instead, 288 // use an algorithm that approximates a minimal Levenshtein distance, like the 289 // one in github.com/google/go-cmp/internal/diff. 290 func (d *differ) diffList(x, y cue.Value) (Kind, *EditScript) { 291 ix, _ := x.List() 292 iy, _ := y.List() 293 294 edits := []Edit{} 295 differs := false 296 i := int32(1) 297 298 for { 299 // TODO: This would be much easier with a Next/Done API. 300 hasX := ix.Next() 301 hasY := iy.Next() 302 if !hasX { 303 for hasY { 304 differs = true 305 edits = append(edits, Edit{UniqueY, 0, i, nil}) 306 hasY = iy.Next() 307 i++ 308 } 309 break 310 } 311 if !hasY { 312 for hasX { 313 differs = true 314 edits = append(edits, Edit{UniqueX, i, 0, nil}) 315 hasX = ix.Next() 316 i++ 317 } 318 break 319 } 320 321 // Both x and y have a value. 322 kind, script := d.diffValue(ix.Value(), iy.Value()) 323 if kind != Identity { 324 differs = true 325 } 326 edits = append(edits, Edit{kind, i, i, script}) 327 i++ 328 } 329 if !differs { 330 return Identity, nil 331 } 332 return Modified, &EditScript{x: x, y: y, edits: edits} 333 }