| package jsondiff |
| |
| import ( |
| "reflect" |
| |
| "github.com/zclconf/go-cty/cty" |
| |
| "github.com/hashicorp/terraform/internal/command/jsonformat/collections" |
| "github.com/hashicorp/terraform/internal/command/jsonformat/computed" |
| "github.com/hashicorp/terraform/internal/command/jsonformat/structured" |
| "github.com/hashicorp/terraform/internal/plans" |
| ) |
| |
| type TransformPrimitiveJson func(before, after interface{}, ctype cty.Type, action plans.Action) computed.Diff |
| type TransformObjectJson func(map[string]computed.Diff, plans.Action) computed.Diff |
| type TransformArrayJson func([]computed.Diff, plans.Action) computed.Diff |
| type TransformUnknownJson func(computed.Diff, plans.Action) computed.Diff |
| type TransformSensitiveJson func(computed.Diff, bool, bool, plans.Action) computed.Diff |
| type TransformTypeChangeJson func(before, after computed.Diff, action plans.Action) computed.Diff |
| |
| // JsonOpts defines the external callback functions that callers should |
| // implement to process the supplied diffs. |
| type JsonOpts struct { |
| Primitive TransformPrimitiveJson |
| Object TransformObjectJson |
| Array TransformArrayJson |
| Unknown TransformUnknownJson |
| Sensitive TransformSensitiveJson |
| TypeChange TransformTypeChangeJson |
| } |
| |
| // Transform accepts a generic before and after value that is assumed to be JSON |
| // formatted and transforms it into a computed.Diff, using the callbacks |
| // supplied in the JsonOpts class. |
| func (opts JsonOpts) Transform(change structured.Change) computed.Diff { |
| if sensitive, ok := opts.processSensitive(change); ok { |
| return sensitive |
| } |
| |
| if unknown, ok := opts.processUnknown(change); ok { |
| return unknown |
| } |
| |
| beforeType := GetType(change.Before) |
| afterType := GetType(change.After) |
| |
| deleted := afterType == Null && !change.AfterExplicit |
| created := beforeType == Null && !change.BeforeExplicit |
| |
| if beforeType == afterType || (created || deleted) { |
| targetType := beforeType |
| if targetType == Null { |
| targetType = afterType |
| } |
| return opts.processUpdate(change, targetType) |
| } |
| |
| b := opts.processUpdate(change.AsDelete(), beforeType) |
| a := opts.processUpdate(change.AsCreate(), afterType) |
| return opts.TypeChange(b, a, plans.Update) |
| } |
| |
| func (opts JsonOpts) processUpdate(change structured.Change, jtype Type) computed.Diff { |
| switch jtype { |
| case Null: |
| return opts.processPrimitive(change, cty.NilType) |
| case Bool: |
| return opts.processPrimitive(change, cty.Bool) |
| case String: |
| return opts.processPrimitive(change, cty.String) |
| case Number: |
| return opts.processPrimitive(change, cty.Number) |
| case Object: |
| return opts.processObject(change.AsMap()) |
| case Array: |
| return opts.processArray(change.AsSlice()) |
| default: |
| panic("unrecognized json type: " + jtype) |
| } |
| } |
| |
| func (opts JsonOpts) processPrimitive(change structured.Change, ctype cty.Type) computed.Diff { |
| beforeMissing := change.Before == nil && !change.BeforeExplicit |
| afterMissing := change.After == nil && !change.AfterExplicit |
| |
| var action plans.Action |
| switch { |
| case beforeMissing && !afterMissing: |
| action = plans.Create |
| case !beforeMissing && afterMissing: |
| action = plans.Delete |
| case reflect.DeepEqual(change.Before, change.After): |
| action = plans.NoOp |
| default: |
| action = plans.Update |
| } |
| |
| return opts.Primitive(change.Before, change.After, ctype, action) |
| } |
| |
| func (opts JsonOpts) processArray(change structured.ChangeSlice) computed.Diff { |
| processIndices := func(beforeIx, afterIx int) computed.Diff { |
| // It's actually really difficult to render the diffs when some indices |
| // within a list are relevant and others aren't. To make this simpler |
| // we just treat all children of a relevant list as also relevant, so we |
| // ignore the relevant attributes field. |
| // |
| // Interestingly the terraform plan builder also agrees with this, and |
| // never sets relevant attributes beneath lists or sets. We're just |
| // going to enforce this logic here as well. If the list is relevant |
| // (decided elsewhere), then every element in the list is also relevant. |
| return opts.Transform(change.GetChild(beforeIx, afterIx)) |
| } |
| |
| isObjType := func(value interface{}) bool { |
| return GetType(value) == Object |
| } |
| |
| return opts.Array(collections.TransformSlice(change.Before, change.After, processIndices, isObjType)) |
| } |
| |
| func (opts JsonOpts) processObject(change structured.ChangeMap) computed.Diff { |
| return opts.Object(collections.TransformMap(change.Before, change.After, change.AllKeys(), func(key string) computed.Diff { |
| child := change.GetChild(key) |
| if !child.RelevantAttributes.MatchesPartial() { |
| child = child.AsNoOp() |
| } |
| |
| return opts.Transform(child) |
| })) |
| } |
| |
| func (opts JsonOpts) processUnknown(change structured.Change) (computed.Diff, bool) { |
| return change.CheckForUnknown( |
| false, |
| func(current structured.Change) computed.Diff { |
| return opts.Unknown(computed.Diff{}, plans.Create) |
| }, func(current structured.Change, before structured.Change) computed.Diff { |
| return opts.Unknown(opts.Transform(before), plans.Update) |
| }, |
| ) |
| } |
| |
| func (opts JsonOpts) processSensitive(change structured.Change) (computed.Diff, bool) { |
| return change.CheckForSensitive(opts.Transform, func(inner computed.Diff, beforeSensitive, afterSensitive bool, action plans.Action) computed.Diff { |
| return opts.Sensitive(inner, beforeSensitive, afterSensitive, action) |
| }) |
| } |