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  }