github.com/gagliardetto/solana-go@v1.11.0/diff/diff.go (about)

     1  // Copyright 2020 dfuse Platform Inc.
     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  	"fmt"
    19  	"reflect"
    20  	"regexp"
    21  	"strings"
    22  
    23  	"github.com/google/go-cmp/cmp"
    24  	"go.uber.org/zap"
    25  )
    26  
    27  type Diffeable interface {
    28  	Diff(right interface{}, options ...Option)
    29  }
    30  
    31  type Option interface {
    32  	apply(o *options)
    33  }
    34  
    35  type optionFunc func(o *options)
    36  
    37  func (f optionFunc) apply(opts *options) {
    38  	f(opts)
    39  }
    40  
    41  func CmpOption(cmpOption cmp.Option) Option {
    42  	return optionFunc(func(opts *options) { opts.cmpOptions = append(opts.cmpOptions, cmpOption) })
    43  }
    44  
    45  func OnEvent(callback func(Event)) Option {
    46  	return optionFunc(func(opts *options) { opts.onEvent = callback })
    47  }
    48  
    49  type options struct {
    50  	cmpOptions []cmp.Option
    51  	onEvent    func(Event)
    52  }
    53  
    54  type Kind uint8
    55  
    56  const (
    57  	KindAdded Kind = iota
    58  	KindChanged
    59  	KindRemoved
    60  )
    61  
    62  func (k Kind) String() string {
    63  	switch k {
    64  	case KindAdded:
    65  		return "added"
    66  	case KindChanged:
    67  		return "changed"
    68  	case KindRemoved:
    69  		return "removed"
    70  	}
    71  
    72  	return "unknown"
    73  }
    74  
    75  type Path cmp.Path
    76  
    77  func (pa Path) SliceIndex() (int, bool) {
    78  	last := pa[len(pa)-1]
    79  	if slcIdx, ok := last.(cmp.SliceIndex); ok {
    80  		xkey, ykey := slcIdx.SplitKeys()
    81  		switch {
    82  		case xkey == ykey:
    83  			return xkey, true
    84  		case ykey == -1:
    85  			// [5->?] means "I don't know where X[5] went"
    86  			return xkey, true
    87  		case xkey == -1:
    88  			// [?->3] means "I don't know where Y[3] came from"
    89  			return ykey, true
    90  		default:
    91  			// [5->3] means "X[5] moved to Y[3]"
    92  			return ykey, true
    93  		}
    94  	}
    95  	return 0, false
    96  }
    97  
    98  func (pa Path) String() string {
    99  	if len(pa) == 1 {
   100  		return ""
   101  	}
   102  
   103  	return strings.TrimPrefix(cmp.Path(pa[1:]).GoString(), ".")
   104  }
   105  
   106  type Event struct {
   107  	Path Path
   108  	Kind Kind
   109  	Old  reflect.Value
   110  	New  reflect.Value
   111  }
   112  
   113  // Match currently simply ensure that `pattern` parameter is the start of the path string
   114  // which represents the direct access from top-level to struct.
   115  func (p *Event) Match(pattern string) (match bool, matches []string) {
   116  	regexRaw := regexp.QuoteMeta(pattern)
   117  	regexRaw = strings.ReplaceAll("^"+regexRaw+"$", "#", `([0-9]+|.->[0-9]+|[0-9]+->.|[0-9]+->[0-9]+)`)
   118  
   119  	return p.RawMatch(regexRaw)
   120  }
   121  
   122  func (p *Event) RawMatch(rawPattern string) (match bool, matches []string) {
   123  	regex := regexp.MustCompile(rawPattern)
   124  	regexMatch := regex.FindAllStringSubmatch(p.Path.String(), 1)
   125  	if len(regexMatch) != 1 {
   126  		return false, nil
   127  	}
   128  
   129  	// For now we accept only array indices, will need to re-write logic if we ever need to check for keys also
   130  	subMatches := regexMatch[0][1:]
   131  	if len(subMatches) == 0 {
   132  		return true, nil
   133  	}
   134  
   135  	return true, subMatches
   136  }
   137  
   138  func (p *Event) AddedKind() bool {
   139  	return p.Kind == KindAdded
   140  }
   141  
   142  func (p *Event) ChangedKind() bool {
   143  	return p.Kind == KindChanged
   144  }
   145  
   146  func (p *Event) RemovedKind() bool {
   147  	return p.Kind == KindRemoved
   148  }
   149  
   150  // Element picks the element based on the Event's Kind, if it's removed, the element is the
   151  // "old" value, if it's added or changed, the element is the "new" value.
   152  func (p *Event) Element() reflect.Value {
   153  	if p.Kind == KindRemoved {
   154  		return p.Old
   155  	}
   156  
   157  	return p.New
   158  }
   159  
   160  func (p *Event) String() string {
   161  	path := ""
   162  	if len(p.Path) > 1 {
   163  		path = " @ " + p.Path.String()
   164  	}
   165  
   166  	return fmt.Sprintf("%s => %s (%s%s)", reflectValueToString(p.Old), reflectValueToString(p.New), p.Kind, path)
   167  }
   168  
   169  func reflectValueToString(value reflect.Value) string {
   170  	if !value.IsValid() {
   171  		return "<nil>"
   172  	}
   173  
   174  	if value.CanInterface() {
   175  		if reflectValueCanIsNil(value) && value.IsNil() {
   176  			return fmt.Sprintf("<nil> (%s)", value.Type())
   177  		}
   178  
   179  		v := value.Interface()
   180  		return fmt.Sprintf("%v (%T)", v, v)
   181  	}
   182  
   183  	return fmt.Sprintf("<type %T>", value.Type())
   184  }
   185  
   186  func reflectValueCanIsNil(value reflect.Value) bool {
   187  	switch value.Kind() {
   188  	case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.UnsafePointer, reflect.Interface, reflect.Slice:
   189  		return true
   190  	default:
   191  		return false
   192  	}
   193  }
   194  
   195  func Diff(left interface{}, right interface{}, opts ...Option) {
   196  	options := options{}
   197  	for _, opt := range opts {
   198  		opt.apply(&options)
   199  	}
   200  
   201  	if options.onEvent == nil {
   202  		panic("the option diff.OnEvent(...) must always be defined")
   203  	}
   204  
   205  	reporter := &diffReporter{notify: options.onEvent}
   206  	cmp.Equal(left, right, append(
   207  		[]cmp.Option{cmp.Reporter(reporter)},
   208  		options.cmpOptions...,
   209  	)...)
   210  }
   211  
   212  type diffReporter struct {
   213  	notify func(event Event)
   214  	path   cmp.Path
   215  	diffs  []string
   216  }
   217  
   218  func (r *diffReporter) PushStep(ps cmp.PathStep) {
   219  	if traceEnabled {
   220  		zlog.Debug("pushing path step", zap.Stringer("step", ps))
   221  	}
   222  
   223  	r.path = append(r.path, ps)
   224  }
   225  
   226  func (r *diffReporter) Report(rs cmp.Result) {
   227  	if !rs.Equal() {
   228  		lastStep := r.path.Last()
   229  		vLeft, vRight := lastStep.Values()
   230  		if !vLeft.IsValid() {
   231  			if traceEnabled {
   232  				zlog.Debug("added event", zap.Stringer("path", r.path))
   233  			}
   234  
   235  			// Left is not set but right is, we have added "right"
   236  			r.notify(Event{Kind: KindAdded, Path: Path(r.path), New: vRight})
   237  			return
   238  		}
   239  
   240  		if !vRight.IsValid() {
   241  			if traceEnabled {
   242  				zlog.Debug("removed event", zap.Stringer("path", r.path))
   243  			}
   244  
   245  			// Left is set but right is not, we have removed "left"
   246  			r.notify(Event{Kind: KindRemoved, Path: Path(r.path), Old: vLeft})
   247  			return
   248  		}
   249  
   250  		if isArrayPathStep(lastStep) {
   251  			// We might want to do this only on certain circumstances?
   252  			if traceEnabled {
   253  				zlog.Debug("array changed event, splitting in removed, added", zap.Stringer("path", r.path))
   254  			}
   255  
   256  			r.notify(Event{Kind: KindRemoved, Path: Path(r.path), Old: vLeft})
   257  			r.notify(Event{Kind: KindAdded, Path: Path(r.path), New: vRight})
   258  			return
   259  		}
   260  
   261  		if traceEnabled {
   262  			zlog.Debug("changed event", zap.Stringer("path", r.path))
   263  		}
   264  
   265  		r.notify(Event{Kind: KindChanged, Path: Path(r.path), Old: vLeft, New: vRight})
   266  	}
   267  }
   268  
   269  func (r *diffReporter) PopStep() {
   270  	if traceEnabled {
   271  		zlog.Debug("popping path step", zap.Stringer("step", r.path[len(r.path)-1]))
   272  	}
   273  
   274  	r.path = r.path[:len(r.path)-1]
   275  }
   276  
   277  func isArrayPathStep(step cmp.PathStep) bool {
   278  	_, ok := step.(cmp.SliceIndex)
   279  	return ok
   280  }
   281  
   282  func copyPath(path cmp.Path) Path {
   283  	if len(path) == 0 {
   284  		return Path(path)
   285  	}
   286  
   287  	out := make([]cmp.PathStep, len(path))
   288  	for i, step := range path {
   289  		out[i] = step
   290  	}
   291  
   292  	return Path(cmp.Path(out))
   293  }