pkg/cloud/api/diff.go (159 lines of code) (raw):

/* Copyright 2023 Google LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package api import ( "fmt" "reflect" ) // TODO: how to diff force send fields? null fields? and zero values? // diff returns a diff between A and B. // // TODO: the behavior of this is not symmetric -- diff(A,B) != diff(B,A). func diff[T any](a, b *T, trait *FieldTraits) (*DiffResult, error) { if trait == nil { trait = &FieldTraits{} } d := &differ[T]{ traits: trait, result: &DiffResult{}, } err := d.do(Path{}, reflect.ValueOf(a), reflect.ValueOf(b)) if err != nil { return nil, err } return d.result, nil } func diffStructs[A any, B any](a *A, b *B) (*DiffResult, error) { d := &differ[A]{ traits: &FieldTraits{}, result: &DiffResult{}, } err := d.do(Path{}, reflect.ValueOf(a), reflect.ValueOf(b)) if err != nil { return nil, err } return d.result, nil } // DiffResult gives a list of elements that differ. type DiffResult struct { Items []DiffItem } // HasDiff is true if the result is has a diff. func (r *DiffResult) HasDiff() bool { return len(r.Items) > 0 } func (r *DiffResult) add(state DiffItemState, p Path, a, b reflect.Value) { di := DiffItem{ State: state, Path: make([]string, len(p)), } copy(di.Path, p) if a.IsValid() { // Interface() will panic if is called on unexported types in this case // the best we can do is to pass its name to the result. if a.CanInterface() { di.A = a.Interface() } else { di.A = a.String() } } if b.IsValid() { // Interface() will panic if is called on unexported types in this case // the best we can do is to pass its name to the result. if b.CanInterface() { di.B = b.Interface() } else { di.B = b.String() } } r.Items = append(r.Items, di) } // DiffItemState gives details on the diff. type DiffItemState string var ( // DiffItemDifferent means the element at the Path differs between A and B. DiffItemDifferent DiffItemState = "Different" // DiffItemOnlyInA means the element at the Path only exists in A, the // value in B is nil. DiffItemOnlyInA DiffItemState = "OnlyInA" // DiffItemOnlyInB means the element at the Path only exists in B, the // value in B is nil. DiffItemOnlyInB DiffItemState = "OnlyInB" ) // DiffItem is an element that is different. type DiffItem struct { State DiffItemState Path Path A any B any } type differ[T any] struct { traits *FieldTraits result *DiffResult } func (d *differ[T]) do(p Path, av, bv reflect.Value) error { // cmpZero applies to pointer, slice and map values. Returns true if no // further diff'ing is required for the values. cmpZero := func() bool { switch { case av.IsZero() && bv.IsZero(): return true case !av.IsZero() && bv.IsZero(): d.result.add(DiffItemOnlyInA, p, av, bv) return true case av.IsZero() && !bv.IsZero(): d.result.add(DiffItemOnlyInB, p, av, bv) return true } return false } switch { case isBasicV(av): if !av.Equal(bv) { d.result.add(DiffItemDifferent, p, av, bv) } return nil case av.Type().Kind() == reflect.Pointer: if cmpZero() { return nil } return d.do(p.Pointer(), av.Elem(), bv.Elem()) case av.Type().Kind() == reflect.Struct: for i := 0; i < av.NumField(); i++ { afv := av.Field(i) aft := av.Type().Field(i) if aft.Name == "NullFields" || aft.Name == "ForceSendFields" { continue } fp := p.Field(aft.Name) switch d.traits.FieldType(fp) { case FieldTypeOutputOnly, FieldTypeSystem: continue } bfv := bv.FieldByName(aft.Name) if !bfv.IsValid() { d.result.add(DiffItemOnlyInA, p, av, bv) continue } if err := d.do(fp, afv, bfv); err != nil { return fmt.Errorf("differ struct %p: %w", fp, err) } } return nil case av.Type().Kind() == reflect.Slice: if cmpZero() { return nil } // If we find the list lengths are difference, don't recurse into a list // to compare item by item. There isn't a use case for a more fine grain // diff within a slice at the moment. if av.Len() != bv.Len() { d.result.add(DiffItemDifferent, p, av, bv) return nil } for i := 0; i < av.Len(); i++ { asv := av.Index(i) bsv := bv.Index(i) sp := p.Index(i) if err := d.do(sp, asv, bsv); err != nil { return fmt.Errorf("differ slice %p: %w", sp, err) } } return nil case av.Type().Kind() == reflect.Map: if cmpZero() { return nil } if av.Len() != bv.Len() { d.result.add(DiffItemDifferent, p, av, bv) return nil } // For maps of the same size, for the maps to be equal, all keys in A // must be present in B for these to be equal. This means we don't have // to check in the opposite direction from B to A. However, this makes // the Diff function non-symmetric. for _, amk := range av.MapKeys() { amv := av.MapIndex(amk) bmv := bv.MapIndex(amk) mp := p.MapIndex(amk) if !bmv.IsValid() { d.result.add(DiffItemDifferent, mp, amv, bmv) } if err := d.do(mp, amv, bmv); err != nil { return fmt.Errorf("differ map %p: %w", mp, err) } } return nil } return fmt.Errorf("differ: invalid type: %s", av.Type()) }