Compare commits

...

9 commits

Author SHA1 Message Date
Ian Wahbe b3633b531a Add support for adds and deletes in detailed diffs
Supporting unit tests are included.
2021-11-24 16:07:57 -08:00
Ian Wahbe 702e4ba6f5 Parameterize TestReplaceOnChanges with engine diff
This commit also adds DetailedDiff to the engine diff.
2021-11-24 13:55:05 -08:00
Ian Wahbe eafc611b43 Assert on change result 2021-11-24 12:27:21 -08:00
Ian Wahbe c7255f44d5 Respect ignore changes
This is performed by calling `processIgnoreChanges` at @pgavlin's
suggestion. Tests are included in this commit.
2021-11-24 12:21:01 -08:00
Ian Wahbe 5ab42ebf08 Add tests & overwrite less of diff 2021-11-24 12:08:16 -08:00
Ian Wahbe 1fc877ff0b Merge branch 'master' into iwahbe/8487/specify-fields-on-engine-diff 2021-11-23 16:47:22 -08:00
Ian Wahbe a2b61cbe57 Respect ignoreChanges 2021-11-23 16:43:51 -08:00
Ian Wahbe 55dfb78ee3 Add CHANGELOG_PENDING.md entry 2021-11-23 16:02:17 -08:00
Ian Wahbe b16ed8a16d Specify fields on engine diff 2021-11-23 15:57:19 -08:00
7 changed files with 474 additions and 142 deletions

View file

@ -8,5 +8,9 @@
### Bug Fixes
- [cli/engine] - Accurately computes the fields changed when diffing with unhelpful providers. This
allows the `replaceOnChanges` feature to be respected for all providers.
[#8488](https://github.com/pulumi/pulumi/pull/8488)
- [codegen/go] - Respect default values in Pulumi object types.
[#8411](https://github.com/pulumi/pulumi/pull/8400)

View file

@ -944,156 +944,147 @@ func TestSingleResourceIgnoreChanges(t *testing.T) {
}, []string{"a", "b"}, []deploy.StepOp{deploy.OpUpdate})
}
func objectDiffToDetailedDiff(prefix string, d *resource.ObjectDiff) map[string]plugin.PropertyDiff {
ret := map[string]plugin.PropertyDiff{}
for k, vd := range d.Updates {
var nestedPrefix string
if prefix == "" {
nestedPrefix = string(k)
} else {
nestedPrefix = fmt.Sprintf("%s.%s", prefix, string(k))
}
for kk, pd := range valueDiffToDetailedDiff(nestedPrefix, vd) {
ret[kk] = pd
}
}
return ret
}
type DiffFunc = func(urn resource.URN, id resource.ID,
olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error)
func arrayDiffToDetailedDiff(prefix string, d *resource.ArrayDiff) map[string]plugin.PropertyDiff {
ret := map[string]plugin.PropertyDiff{}
for i, vd := range d.Updates {
for kk, pd := range valueDiffToDetailedDiff(fmt.Sprintf("%s[%d]", prefix, i), vd) {
ret[kk] = pd
func replaceOnChangesTest(t *testing.T, name string, diffFunc DiffFunc) {
t.Run(name, func(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
DiffF: diffFunc,
UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64,
ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) {
return news, resource.StatusOK, nil
},
CreateF: func(urn resource.URN, inputs resource.PropertyMap, timeout float64,
preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) {
return resource.ID("id123"), inputs, resource.StatusOK, nil
},
}, nil
}),
}
}
return ret
}
func valueDiffToDetailedDiff(prefix string, vd resource.ValueDiff) map[string]plugin.PropertyDiff {
ret := map[string]plugin.PropertyDiff{}
if vd.Object != nil {
for kk, pd := range objectDiffToDetailedDiff(prefix, vd.Object) {
ret[kk] = pd
updateProgramWithProps := func(snap *deploy.Snapshot, props resource.PropertyMap, replaceOnChanges []string,
allowedOps []deploy.StepOp) *deploy.Snapshot {
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
Inputs: props,
ReplaceOnChanges: replaceOnChanges,
})
assert.NoError(t, err)
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
Steps: []TestStep{
{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
events []Event, res result.Result) result.Result {
for _, event := range events {
if event.Type == ResourcePreEvent {
payload := event.Payload().(ResourcePreEventPayload)
assert.Subset(t, allowedOps, []deploy.StepOp{payload.Metadata.Op})
}
}
return res
},
},
},
}
return p.Run(t, snap)
}
} else if vd.Array != nil {
for kk, pd := range arrayDiffToDetailedDiff(prefix, vd.Array) {
ret[kk] = pd
snap := updateProgramWithProps(nil, resource.NewPropertyMapFromMap(map[string]interface{}{
"a": 1,
"b": map[string]interface{}{
"c": "foo",
},
}), []string{"a", "b.c"}, []deploy.StepOp{deploy.OpCreate})
// Ensure that a change to a replaceOnChange property results in an OpReplace
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
"a": 2,
"b": map[string]interface{}{
"c": "foo",
},
}), []string{"a"}, []deploy.StepOp{deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced})
// Ensure that a change to a nested replaceOnChange property results in an OpReplace
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
"a": 2,
"b": map[string]interface{}{
"c": "bar",
},
}), []string{"b.c"}, []deploy.StepOp{deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced})
// Ensure that a change to any property of a "*" replaceOnChange results in an OpReplace
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
"a": 3,
"b": map[string]interface{}{
"c": "baz",
},
}), []string{"*"}, []deploy.StepOp{deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced})
// Ensure that a change to an non-replaceOnChange property results in an OpUpdate
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
"a": 4,
"b": map[string]interface{}{
"c": "qux",
},
}), nil, []deploy.StepOp{deploy.OpUpdate})
// We ensure that we are listing to the engine diff function only when the provider function
// is nil. We do this by adding some weirdness to the provider diff function.
allowed := []deploy.StepOp{deploy.OpCreateReplacement, deploy.OpReplace, deploy.OpDeleteReplaced}
if diffFunc != nil {
allowed = []deploy.StepOp{deploy.OpSame}
}
} else {
ret[prefix] = plugin.PropertyDiff{Kind: plugin.DiffUpdate}
}
return ret
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
"a": 42, // 42 is a special value in the "provider" diff function.
"b": map[string]interface{}{
"c": "qux",
},
}), []string{"a"}, allowed)
_ = snap
})
}
func TestReplaceOnChanges(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
DiffF: func(urn resource.URN, id resource.ID,
olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) {
diff := olds.Diff(news)
if diff == nil {
return plugin.DiffResult{Changes: plugin.DiffNone}, nil
}
detailedDiff := objectDiffToDetailedDiff("", diff)
var changedKeys []resource.PropertyKey
for _, k := range diff.Keys() {
if diff.Changed(k) {
changedKeys = append(changedKeys, k)
}
}
return plugin.DiffResult{
Changes: plugin.DiffSome,
ChangedKeys: changedKeys,
DetailedDiff: detailedDiff,
}, nil
},
UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64,
ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) {
return news, resource.StatusOK, nil
},
CreateF: func(urn resource.URN, inputs resource.PropertyMap, timeout float64,
preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) {
return resource.ID("id123"), inputs, resource.StatusOK, nil
},
// We simulate a provider that has it's own diff function.
replaceOnChangesTest(t, "provider diff",
func(urn resource.URN, id resource.ID,
olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) {
// To establish a observable difference between the provider and engine diff function,
// we treat 42 as an OpSame. We use this to check that the right diff function is being
// used.
for k, v := range news {
if v == resource.NewNumberProperty(42) {
news[k] = olds[k]
}
}
diff := olds.Diff(news)
if diff == nil {
return plugin.DiffResult{Changes: plugin.DiffNone}, nil
}
detailedDiff := plugin.NewDetailedDiffFromObjectDiff(diff)
changedKeys := diff.ChangedKeys()
return plugin.DiffResult{
Changes: plugin.DiffSome,
ChangedKeys: changedKeys,
DetailedDiff: detailedDiff,
}, nil
}),
}
updateProgramWithProps := func(snap *deploy.Snapshot, props resource.PropertyMap, replaceOnChanges []string,
allowedOps []deploy.StepOp) *deploy.Snapshot {
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
Inputs: props,
ReplaceOnChanges: replaceOnChanges,
})
assert.NoError(t, err)
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
Steps: []TestStep{
{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
events []Event, res result.Result) result.Result {
for _, event := range events {
if event.Type == ResourcePreEvent {
payload := event.Payload().(ResourcePreEventPayload)
assert.Subset(t, allowedOps, []deploy.StepOp{payload.Metadata.Op})
}
}
return res
},
},
},
}
return p.Run(t, snap)
}
snap := updateProgramWithProps(nil, resource.NewPropertyMapFromMap(map[string]interface{}{
"a": 1,
"b": map[string]interface{}{
"c": "foo",
},
}), []string{"a", "b.c"}, []deploy.StepOp{deploy.OpCreate})
// Ensure that a change to a replaceOnChange property results in an OpReplace
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
"a": 2,
"b": map[string]interface{}{
"c": "foo",
},
}), []string{"a"}, []deploy.StepOp{deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced})
// Ensure that a change to a nested replaceOnChange property results in an OpReplace
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
"a": 2,
"b": map[string]interface{}{
"c": "bar",
},
}), []string{"b.c"}, []deploy.StepOp{deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced})
// Ensure that a change to any property of a "*" replaceOnChange results in an OpReplace
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
"a": 3,
"b": map[string]interface{}{
"c": "baz",
},
}), []string{"*"}, []deploy.StepOp{deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced})
// Ensure that a change to an non-replaceOnChange property results in an OpUpdate
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
"a": 4,
"b": map[string]interface{}{
"c": "qux",
},
}), nil, []deploy.StepOp{deploy.OpUpdate})
_ = snap
// We simulate a provider that does not have it's own diff function. This tests the engines diff
// function instead.
replaceOnChangesTest(t, "engine diff", nil)
}
// Resource is an abstract representation of a resource graph

View file

@ -1132,10 +1132,17 @@ func diffResource(urn resource.URN, id resource.ID, oldInputs, oldOutputs,
return diff, err
}
if diff.Changes == plugin.DiffUnknown {
if oldInputs.DeepEquals(newInputs) {
diff.Changes = plugin.DiffNone
} else {
new, res := processIgnoreChanges(newInputs, oldInputs, ignoreChanges)
if res != nil {
return plugin.DiffResult{}, err
}
tmp := oldInputs.Diff(new)
if tmp.AnyChanges() {
diff.Changes = plugin.DiffSome
diff.ChangedKeys = tmp.ChangedKeys()
diff.DetailedDiff = plugin.NewDetailedDiffFromObjectDiff(tmp)
} else {
diff.Changes = plugin.DiffNone
}
}
return diff, nil

View file

@ -1,8 +1,23 @@
// Copyright 2016-2021, Pulumi Corporation.
//
// 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
//
// http://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 deploy
import (
"testing"
"github.com/pulumi/pulumi/pkg/v3/resource/deploy/deploytest"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
"github.com/stretchr/testify/assert"
@ -194,3 +209,93 @@ func TestApplyReplaceOnChangesEmptyDetailedDiff(t *testing.T) {
}
}
func TestEngineDiff(t *testing.T) {
cases := []struct {
name string
oldInputs, newInputs resource.PropertyMap
ignoreChanges []string
expected []resource.PropertyKey
expectedChanges plugin.DiffChanges
}{
{
name: "Empty diff",
oldInputs: resource.NewPropertyMapFromMap(map[string]interface{}{
"val1": resource.NewPropertyValue(8),
"val2": resource.NewPropertyValue("hello"),
}),
newInputs: resource.NewPropertyMapFromMap(map[string]interface{}{
"val1": resource.NewPropertyValue(8),
"val2": resource.NewPropertyValue("hello"),
}),
expected: nil,
expectedChanges: plugin.DiffNone,
},
{
name: "All changes",
oldInputs: resource.NewPropertyMapFromMap(map[string]interface{}{
"val0": resource.NewPropertyValue(3.14),
}),
newInputs: resource.NewPropertyMapFromMap(map[string]interface{}{
"val1": resource.NewNumberProperty(42),
"val2": resource.NewPropertyValue("world"),
}),
expected: []resource.PropertyKey{"val0", "val1", "val2"},
expectedChanges: plugin.DiffSome,
},
{
name: "Some changes",
oldInputs: resource.NewPropertyMapFromMap(map[string]interface{}{
"val1": resource.NewPropertyValue(42),
}),
newInputs: resource.NewPropertyMapFromMap(map[string]interface{}{
"val1": resource.NewNumberProperty(42),
"val2": resource.NewPropertyValue("world"),
}),
expected: []resource.PropertyKey{"val2"},
expectedChanges: plugin.DiffSome,
},
{
name: "Ignore some changes",
oldInputs: resource.NewPropertyMapFromMap(map[string]interface{}{
"val1": resource.NewPropertyValue("hello"),
}),
newInputs: resource.NewPropertyMapFromMap(map[string]interface{}{
"val2": resource.NewPropertyValue(8),
}),
ignoreChanges: []string{"val1"},
expected: []resource.PropertyKey{"val2"},
expectedChanges: plugin.DiffSome,
},
{
name: "Ignore all changes",
oldInputs: resource.NewPropertyMapFromMap(map[string]interface{}{
"val1": resource.NewPropertyValue("hello"),
}),
newInputs: resource.NewPropertyMapFromMap(map[string]interface{}{
"val2": resource.NewPropertyValue(8),
}),
ignoreChanges: []string{"val1", "val2"},
expected: nil,
expectedChanges: plugin.DiffNone,
},
}
urn := resource.URN("urn:pulumi:dev::website-and-lambda::aws:s3/bucket:Bucket::my-bucket")
id := resource.ID("someid")
var oldOutputs resource.PropertyMap
allowUnknowns := false
provider := deploytest.Provider{}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
diff, err := diffResource(urn, id, c.oldInputs, oldOutputs, c.newInputs, &provider, allowUnknowns, c.ignoreChanges)
t.Logf("diff.ChangedKeys = %v", diff.ChangedKeys)
t.Logf("diff.StableKeys = %v", diff.StableKeys)
t.Logf("diff.ReplaceKeys = %v", diff.ReplaceKeys)
assert.NoError(t, err)
assert.Equal(t, c.expectedChanges, diff.Changes)
assert.EqualValues(t, c.expected, diff.ChangedKeys)
})
}
}

View file

@ -1,4 +1,4 @@
// Copyright 2016-2018, Pulumi Corporation.
// Copyright 2016-2021, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -16,6 +16,7 @@ package plugin
import (
"errors"
"fmt"
"io"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
@ -217,6 +218,85 @@ type DiffResult struct {
DeleteBeforeReplace bool // if true, this resource must be deleted before recreating it.
}
// Computes the detailed diff of Updated, Added and Deleted keys.
func NewDetailedDiffFromObjectDiff(diff *resource.ObjectDiff) map[string]PropertyDiff {
if diff == nil {
return map[string]PropertyDiff{}
}
return objectDiffToDetailedDiff("", diff)
}
func objectDiffToDetailedDiff(prefix string, diff *resource.ObjectDiff) map[string]PropertyDiff {
ret := map[string]PropertyDiff{}
getPrefix := func(k resource.PropertyKey) string {
if prefix == "" {
return string(k)
}
return fmt.Sprintf("%s.%s", prefix, string(k))
}
for k, vd := range diff.Updates {
nestedPrefix := getPrefix(k)
for kk, pd := range valueDiffToDetailedDiff(nestedPrefix, vd) {
ret[kk] = pd
}
}
for k := range diff.Adds {
nestedPrefix := getPrefix(k)
ret[nestedPrefix] = PropertyDiff{Kind: DiffAdd}
}
for k := range diff.Deletes {
nestedPrefix := getPrefix(k)
ret[nestedPrefix] = PropertyDiff{Kind: DiffDelete}
}
return ret
}
func arrayDiffToDetailedDiff(prefix string, d *resource.ArrayDiff) map[string]PropertyDiff {
nestedPrefix := func(i int) string { return fmt.Sprintf("%s[%d]", prefix, i) }
ret := map[string]PropertyDiff{}
for i, vd := range d.Updates {
for kk, pd := range valueDiffToDetailedDiff(nestedPrefix(i), vd) {
ret[kk] = pd
}
}
for i := range d.Adds {
ret[nestedPrefix(i)] = PropertyDiff{Kind: DiffAdd}
}
for i := range d.Deletes {
ret[nestedPrefix(i)] = PropertyDiff{Kind: DiffDelete}
}
return ret
}
func valueDiffToDetailedDiff(prefix string, vd resource.ValueDiff) map[string]PropertyDiff {
ret := map[string]PropertyDiff{}
if vd.Object != nil {
for kk, pd := range objectDiffToDetailedDiff(prefix, vd.Object) {
ret[kk] = pd
}
} else if vd.Array != nil {
for kk, pd := range arrayDiffToDetailedDiff(prefix, vd.Array) {
ret[kk] = pd
}
} else {
switch {
case vd.Old.V == nil && vd.New.V != nil:
ret[prefix] = PropertyDiff{Kind: DiffAdd}
case vd.Old.V != nil && vd.New.V == nil:
ret[prefix] = PropertyDiff{Kind: DiffDelete}
default:
ret[prefix] = PropertyDiff{Kind: DiffUpdate}
}
}
return ret
}
// Replace returns true if this diff represents a replacement.
func (r DiffResult) Replace() bool {
for _, v := range r.DetailedDiff {

View file

@ -0,0 +1,126 @@
// Copyright 2016-2021, Pulumi Corporation.
//
// 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
//
// http://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 plugin
import (
"testing"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/stretchr/testify/assert"
)
func TestNewDetailedDiff(t *testing.T) {
cases := []struct {
name string
diff *resource.ObjectDiff
expected map[string]PropertyDiff
}{
{
name: "updates",
diff: resource.NewPropertyMapFromMap(map[string]interface{}{
"a": 1,
"b": map[string]interface{}{
"c": 2,
"d": 3,
},
}).Diff(resource.NewPropertyMapFromMap(map[string]interface{}{
"a": -1,
"b": map[string]interface{}{
"c": -2,
"d": 3,
},
})),
expected: map[string]PropertyDiff{
"a": {
Kind: DiffUpdate,
},
"b.c": {
Kind: DiffUpdate,
},
},
},
{
name: "adds and deletes",
diff: resource.NewPropertyMapFromMap(map[string]interface{}{
"b": map[string]interface{}{
"c": 2,
"d": 3,
},
}).Diff(resource.NewPropertyMapFromMap(map[string]interface{}{
"a": 1,
"b": map[string]interface{}{
"d": 3,
},
})),
expected: map[string]PropertyDiff{
"a": {
Kind: DiffAdd,
},
"b.c": {
Kind: DiffDelete,
},
},
},
{
name: "arrays",
diff: resource.NewPropertyMapFromMap(map[string]interface{}{
"a": []interface{}{
map[string]interface{}{
"a": 1,
"b": []interface{}{
2,
3,
},
},
},
}).Diff(resource.NewPropertyMapFromMap(
map[string]interface{}{
"a": []interface{}{
map[string]interface{}{
"a": -1,
"b": []interface{}{
2,
},
},
4,
},
})),
expected: map[string]PropertyDiff{
"a[0].a": {
Kind: DiffUpdate,
},
"a[0].b[1]": {
Kind: DiffDelete,
},
"a[1]": {
Kind: DiffAdd,
},
},
},
{
name: "nil diff",
diff: nil,
expected: map[string]PropertyDiff{},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
actual := NewDetailedDiffFromObjectDiff(c.diff)
assert.Equal(t, c.expected, actual)
})
}
}

View file

@ -55,6 +55,12 @@ func (diff *ObjectDiff) Same(k PropertyKey) bool {
return !diff.Changed(k)
}
// Returns true if there are no changes (adds, deletes, updates) in the diff. Also returns true if
// diff is nil. Otherwise returns false.
func (diff *ObjectDiff) AnyChanges() bool {
return diff != nil && len(diff.Adds)+len(diff.Deletes)+len(diff.Updates) > 0
}
// Keys returns a stable snapshot of all keys known to this object, across adds, deletes, sames, and updates.
func (diff *ObjectDiff) Keys() []PropertyKey {
var ks []PropertyKey
@ -74,6 +80,19 @@ func (diff *ObjectDiff) Keys() []PropertyKey {
return ks
}
// All keys where Changed(k) = true.
func (diff *ObjectDiff) ChangedKeys() []PropertyKey {
var ks []PropertyKey
if diff != nil {
for _, k := range diff.Keys() {
if diff.Changed(k) {
ks = append(ks, k)
}
}
}
return ks
}
// ValueDiff holds the results of diffing two property values.
type ValueDiff struct {
Old PropertyValue // the old value.