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 }