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  }