cuelang.org/go@v0.10.1/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 // Len returns the number of edits. 88 func (es *EditScript) Len() int { 89 return len(es.edits) 90 } 91 92 // Label returns a string representation of the label. 93 func (es *EditScript) LabelX(i int) string { 94 e := es.edits[i] 95 p := e.XPos() 96 if p < 0 { 97 return "" 98 } 99 return label(es.x, p) 100 } 101 102 func (es *EditScript) LabelY(i int) string { 103 e := es.edits[i] 104 p := e.YPos() 105 if p < 0 { 106 return "" 107 } 108 return label(es.y, p) 109 } 110 111 // TODO: support label expressions. 112 func label(v cue.Value, i int) string { 113 st, err := v.Struct() 114 if err != nil { 115 return "" 116 } 117 118 // TODO: return formatted expression for optionals. 119 f := st.Field(i) 120 str := f.Selector 121 if f.IsOptional { 122 str += "?" 123 } 124 str += ":" 125 return str 126 } 127 128 // ValueX returns the value of X involved at step i. 129 func (es *EditScript) ValueX(i int) (v cue.Value) { 130 p := es.edits[i].XPos() 131 if p < 0 { 132 return v 133 } 134 st, err := es.x.Struct() 135 if err != nil { 136 return v 137 } 138 return st.Field(p).Value 139 } 140 141 // ValueY returns the value of Y involved at step i. 142 func (es *EditScript) ValueY(i int) (v cue.Value) { 143 p := es.edits[i].YPos() 144 if p < 0 { 145 return v 146 } 147 st, err := es.y.Struct() 148 if err != nil { 149 return v 150 } 151 return st.Field(p).Value 152 } 153 154 // Edit represents a single operation within an edit-script. 155 type Edit struct { 156 kind Kind 157 xPos int32 // 0 if UniqueY 158 yPos int32 // 0 if UniqueX 159 sub *EditScript // non-nil if Modified 160 } 161 162 func (e Edit) Kind() Kind { return e.kind } 163 func (e Edit) XPos() int { return int(e.xPos - 1) } 164 func (e Edit) YPos() int { return int(e.yPos - 1) } 165 166 type differ struct { 167 cfg Profile 168 } 169 170 func (d *differ) diffValue(x, y cue.Value) (Kind, *EditScript) { 171 if d.cfg.Concrete { 172 x, _ = x.Default() 173 y, _ = y.Default() 174 } 175 if x.IncompleteKind() != y.IncompleteKind() { 176 return Modified, nil 177 } 178 179 switch xc, yc := x.IsConcrete(), y.IsConcrete(); { 180 case xc != yc: 181 return Modified, nil 182 183 case xc && yc: 184 switch k := x.Kind(); k { 185 case cue.StructKind: 186 return d.diffStruct(x, y) 187 188 case cue.ListKind: 189 return d.diffList(x, y) 190 } 191 fallthrough 192 193 default: 194 // In concrete mode we do not care about non-concrete values. 195 if d.cfg.Concrete { 196 return Identity, nil 197 } 198 199 if !x.Equals(y) { 200 return Modified, nil 201 } 202 } 203 204 return Identity, nil 205 } 206 207 func (d *differ) field(s *cue.Struct, i int) (_ cue.FieldInfo, ok bool) { 208 f := s.Field(i) 209 if d.cfg.SkipHidden && f.IsHidden { 210 return cue.FieldInfo{}, false 211 } 212 return f, true 213 } 214 215 func (d *differ) diffStruct(x, y cue.Value) (Kind, *EditScript) { 216 sx, _ := x.Struct() 217 sy, _ := y.Struct() 218 219 // Best-effort topological sort, prioritizing x over y, using a variant of 220 // Kahn's algorithm (see, for instance 221 // https://www.geeksforgeeks.org/topological-sorting-indegree-based-solution/). 222 // We assume that the order of the elements of each value indicate an edge 223 // in the graph. This means that only the next unprocessed nodes can be 224 // those with no incoming edges. 225 xMap := make(map[string]int32, sx.Len()) 226 yMap := make(map[string]int32, sy.Len()) 227 for i := 0; i < sx.Len(); i++ { 228 f, ok := d.field(sx, i) 229 if !ok { 230 continue 231 } 232 xMap[f.Selector] = int32(i + 1) 233 } 234 for i := 0; i < sy.Len(); i++ { 235 f, ok := d.field(sy, i) 236 if !ok { 237 continue 238 } 239 yMap[f.Selector] = int32(i + 1) 240 } 241 242 edits := []Edit{} 243 differs := false 244 245 var xi, yi int 246 var xf, yf cue.FieldInfo 247 var ok bool 248 for xi < sx.Len() || yi < sy.Len() { 249 // Process zero nodes 250 for ; xi < sx.Len(); xi++ { 251 xf, ok = d.field(sx, xi) 252 if !ok { 253 continue 254 } 255 yp := yMap[xf.Selector] 256 if yp > 0 { 257 break 258 } 259 edits = append(edits, Edit{UniqueX, int32(xi + 1), 0, nil}) 260 differs = true 261 } 262 for ; yi < sy.Len(); yi++ { 263 yf, ok = d.field(sy, yi) 264 if !ok { 265 continue 266 } 267 if yMap[yf.Selector] == 0 { 268 // already done 269 continue 270 } 271 xp := xMap[yf.Selector] 272 if xp > 0 { 273 break 274 } 275 yMap[yf.Selector] = 0 276 edits = append(edits, Edit{UniqueY, 0, int32(yi + 1), nil}) 277 differs = true 278 } 279 280 // Compare nodes 281 var ok bool 282 for ; xi < sx.Len(); xi++ { 283 xf, ok = d.field(sx, xi) 284 if !ok { 285 continue 286 } 287 288 yp := yMap[xf.Selector] 289 if yp == 0 { 290 break 291 } 292 // If yp != xi+1, the topological sort was not possible. 293 yMap[xf.Selector] = 0 294 295 yf, ok := d.field(sy, int(yp-1)) 296 if !ok { 297 continue 298 } 299 300 kind := Identity 301 var script *EditScript 302 switch { 303 case xf.IsDefinition != yf.IsDefinition, 304 xf.IsOptional != yf.IsOptional: 305 kind = Modified 306 307 default: 308 xv := xf.Value 309 yv := yf.Value 310 // TODO(perf): consider evaluating lazily. 311 kind, script = d.diffValue(xv, yv) 312 } 313 314 edits = append(edits, Edit{kind, int32(xi + 1), yp, script}) 315 if kind != Identity { 316 differs = true 317 } 318 } 319 } 320 if !differs { 321 return Identity, nil 322 } 323 return Modified, &EditScript{x: x, y: y, edits: edits} 324 } 325 326 // TODO: right now we do a simple element-by-element comparison. Instead, 327 // use an algorithm that approximates a minimal Levenshtein distance, like the 328 // one in github.com/google/go-cmp/internal/diff. 329 func (d *differ) diffList(x, y cue.Value) (Kind, *EditScript) { 330 ix, _ := x.List() 331 iy, _ := y.List() 332 333 edits := []Edit{} 334 differs := false 335 i := int32(1) 336 337 for { 338 // TODO: This would be much easier with a Next/Done API. 339 hasX := ix.Next() 340 hasY := iy.Next() 341 if !hasX { 342 for hasY { 343 differs = true 344 edits = append(edits, Edit{UniqueY, 0, i, nil}) 345 hasY = iy.Next() 346 i++ 347 } 348 break 349 } 350 if !hasY { 351 for hasX { 352 differs = true 353 edits = append(edits, Edit{UniqueX, i, 0, nil}) 354 hasX = ix.Next() 355 i++ 356 } 357 break 358 } 359 360 // Both x and y have a value. 361 kind, script := d.diffValue(ix.Value(), iy.Value()) 362 if kind != Identity { 363 differs = true 364 } 365 edits = append(edits, Edit{kind, i, i, script}) 366 i++ 367 } 368 if !differs { 369 return Identity, nil 370 } 371 return Modified, &EditScript{x: x, y: y, edits: edits} 372 }