| // Copyright (c) The OpenTofu Authors | |
| // SPDX-License-Identifier: MPL-2.0 | |
| // Copyright (c) 2023 HashiCorp, Inc. | |
| // SPDX-License-Identifier: MPL-2.0 | |
| package renderers | |
| import ( | |
| "encoding/json" | |
| "fmt" | |
| "strings" | |
| "github.com/zclconf/go-cty/cty" | |
| "github.com/opentofu/opentofu/internal/command/jsonformat/collections" | |
| "github.com/opentofu/opentofu/internal/command/jsonformat/computed" | |
| "github.com/opentofu/opentofu/internal/command/jsonformat/structured" | |
| "github.com/opentofu/opentofu/internal/command/jsonformat/structured/attribute_path" | |
| "github.com/opentofu/opentofu/internal/plans" | |
| ) | |
| var _ computed.DiffRenderer = (*primitiveRenderer)(nil) | |
| func Primitive(before, after interface{}, ctype cty.Type) computed.DiffRenderer { | |
| return &primitiveRenderer{ | |
| before: before, | |
| after: after, | |
| ctype: ctype, | |
| } | |
| } | |
| type primitiveRenderer struct { | |
| NoWarningsRenderer | |
| before interface{} | |
| after interface{} | |
| ctype cty.Type | |
| } | |
| func (renderer primitiveRenderer) RenderHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) string { | |
| if renderer.ctype == cty.String { | |
| return renderer.renderStringDiff(diff, indent, opts) | |
| } | |
| beforeValue := renderPrimitiveValue(renderer.before, renderer.ctype, opts) | |
| afterValue := renderPrimitiveValue(renderer.after, renderer.ctype, opts) | |
| switch diff.Action { | |
| case plans.Create: | |
| return fmt.Sprintf("%s%s", afterValue, forcesReplacement(diff.Replace, opts)) | |
| case plans.Delete: | |
| return fmt.Sprintf("%s%s%s", beforeValue, nullSuffix(diff.Action, opts), forcesReplacement(diff.Replace, opts)) | |
| case plans.NoOp: | |
| return fmt.Sprintf("%s%s", beforeValue, forcesReplacement(diff.Replace, opts)) | |
| default: | |
| return fmt.Sprintf("%s %s %s%s", beforeValue, opts.Colorize.Color("[yellow]->[reset]"), afterValue, forcesReplacement(diff.Replace, opts)) | |
| } | |
| } | |
| func renderPrimitiveValue(value interface{}, t cty.Type, opts computed.RenderHumanOpts) string { | |
| if value == nil { | |
| return opts.Colorize.Color("[dark_gray]null[reset]") | |
| } | |
| switch t { | |
| case cty.Bool: | |
| if value.(bool) { | |
| return "true" | |
| } | |
| return "false" | |
| case cty.Number: | |
| num := value.(json.Number) | |
| return num.String() | |
| default: | |
| panic("unrecognized primitive type: " + t.FriendlyName()) | |
| } | |
| } | |
| func (renderer primitiveRenderer) renderStringDiff(diff computed.Diff, indent int, opts computed.RenderHumanOpts) string { | |
| // We process multiline strings at the end of the switch statement. | |
| var lines []string | |
| switch diff.Action { | |
| case plans.Create, plans.NoOp: | |
| str := evaluatePrimitiveString(renderer.after, opts) | |
| if str.Json != nil { | |
| if diff.Action == plans.NoOp { | |
| return renderer.renderStringDiffAsJson(diff, indent, opts, str, str) | |
| } else { | |
| return renderer.renderStringDiffAsJson(diff, indent, opts, evaluatedString{}, str) | |
| } | |
| } | |
| if !str.IsMultiline { | |
| return fmt.Sprintf("%s%s", str.RenderSimple(), forcesReplacement(diff.Replace, opts)) | |
| } | |
| // We are creating a single multiline string, so let's split by the new | |
| // line character. While we are doing this, we are going to insert our | |
| // indents and make sure each line is formatted correctly. | |
| lines = strings.Split(strings.ReplaceAll(str.String, "\n", fmt.Sprintf("\n%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts))), "\n") | |
| // We now just need to do the same for the first entry in lines, because | |
| // we split on the new line characters which won't have been at the | |
| // beginning of the first line. | |
| lines[0] = fmt.Sprintf("%s%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), lines[0]) | |
| case plans.Delete: | |
| str := evaluatePrimitiveString(renderer.before, opts) | |
| if str.IsNull { | |
| // We don't put the null suffix (-> null) here because the final | |
| // render or null -> null would look silly. | |
| return fmt.Sprintf("%s%s", str.RenderSimple(), forcesReplacement(diff.Replace, opts)) | |
| } | |
| if str.Json != nil { | |
| return renderer.renderStringDiffAsJson(diff, indent, opts, str, evaluatedString{}) | |
| } | |
| if !str.IsMultiline { | |
| return fmt.Sprintf("%s%s%s", str.RenderSimple(), nullSuffix(diff.Action, opts), forcesReplacement(diff.Replace, opts)) | |
| } | |
| // We are creating a single multiline string, so let's split by the new | |
| // line character. While we are doing this, we are going to insert our | |
| // indents and make sure each line is formatted correctly. | |
| lines = strings.Split(strings.ReplaceAll(str.String, "\n", fmt.Sprintf("\n%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts))), "\n") | |
| // We now just need to do the same for the first entry in lines, because | |
| // we split on the new line characters which won't have been at the | |
| // beginning of the first line. | |
| lines[0] = fmt.Sprintf("%s%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), lines[0]) | |
| default: | |
| beforeString := evaluatePrimitiveString(renderer.before, opts) | |
| afterString := evaluatePrimitiveString(renderer.after, opts) | |
| if beforeString.Json != nil && afterString.Json != nil { | |
| return renderer.renderStringDiffAsJson(diff, indent, opts, beforeString, afterString) | |
| } | |
| if beforeString.Json != nil || afterString.Json != nil { | |
| // This means one of the strings is JSON and one isn't. We're going | |
| // to be a little inefficient here, but we can just reuse another | |
| // renderer for this so let's keep it simple. | |
| return computed.NewDiff( | |
| TypeChange( | |
| computed.NewDiff(Primitive(renderer.before, nil, cty.String), plans.Delete, false), | |
| computed.NewDiff(Primitive(nil, renderer.after, cty.String), plans.Create, false)), | |
| diff.Action, | |
| diff.Replace).RenderHuman(indent, opts) | |
| } | |
| if !beforeString.IsMultiline && !afterString.IsMultiline { | |
| return fmt.Sprintf("%s %s %s%s", beforeString.RenderSimple(), opts.Colorize.Color("[yellow]->[reset]"), afterString.RenderSimple(), forcesReplacement(diff.Replace, opts)) | |
| } | |
| beforeLines := strings.Split(beforeString.String, "\n") | |
| afterLines := strings.Split(afterString.String, "\n") | |
| processIndices := func(beforeIx, afterIx int) { | |
| if beforeIx < 0 || beforeIx >= len(beforeLines) { | |
| lines = append(lines, fmt.Sprintf("%s%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.Create, opts), afterLines[afterIx])) | |
| return | |
| } | |
| if afterIx < 0 || afterIx >= len(afterLines) { | |
| lines = append(lines, fmt.Sprintf("%s%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.Delete, opts), beforeLines[beforeIx])) | |
| return | |
| } | |
| lines = append(lines, fmt.Sprintf("%s%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), beforeLines[beforeIx])) | |
| } | |
| isObjType := func(_, _ string) bool { | |
| return false | |
| } | |
| collections.ProcessSlice(beforeLines, afterLines, processIndices, isObjType) | |
| } | |
| // We return early if we find non-multiline strings or JSON strings, so we | |
| // know here that we just render the lines slice properly. | |
| return fmt.Sprintf("<<-EOT%s\n%s\n%s%sEOT%s", | |
| forcesReplacement(diff.Replace, opts), | |
| strings.Join(lines, "\n"), | |
| formatIndent(indent), | |
| writeDiffActionSymbol(plans.NoOp, opts), | |
| nullSuffix(diff.Action, opts)) | |
| } | |
| func (renderer primitiveRenderer) renderStringDiffAsJson(diff computed.Diff, indent int, opts computed.RenderHumanOpts, before evaluatedString, after evaluatedString) string { | |
| jsonDiff := RendererJsonOpts().Transform(structured.Change{ | |
| BeforeExplicit: diff.Action != plans.Create, | |
| AfterExplicit: diff.Action != plans.Delete, | |
| Before: before.Json, | |
| After: after.Json, | |
| Unknown: false, | |
| BeforeSensitive: false, | |
| AfterSensitive: false, | |
| ReplacePaths: attribute_path.Empty(false), | |
| RelevantAttributes: attribute_path.AlwaysMatcher(), | |
| }) | |
| action := diff.Action | |
| jsonOpts := opts.Clone() | |
| jsonOpts.OverrideNullSuffix = true | |
| var whitespace, replace string | |
| if jsonDiff.Action == plans.NoOp && diff.Action == plans.Update { | |
| // Then this means we are rendering a whitespace only change. The JSON | |
| // differ will have ignored the whitespace changes so that makes the | |
| // diff we are about to print out very confusing without extra | |
| // explanation. | |
| if diff.Replace { | |
| whitespace = " # whitespace changes force replacement" | |
| } else { | |
| whitespace = " # whitespace changes" | |
| } | |
| // Because we'd be showing no changes otherwise: | |
| jsonOpts.ShowUnchangedChildren = true | |
| // Whitespace changes should not appear as if edited. | |
| action = plans.NoOp | |
| } else { | |
| // We only show the replace suffix if we didn't print something out | |
| // about whitespace changes. | |
| replace = forcesReplacement(diff.Replace, opts) | |
| } | |
| renderedJsonDiff := jsonDiff.RenderHuman(indent+1, jsonOpts) | |
| if diff.Action == plans.Create || diff.Action == plans.Delete { | |
| // We don't display the '+' or '-' symbols on the JSON diffs, we should | |
| // still display the '~' for an update action though. | |
| action = plans.NoOp | |
| } | |
| if strings.Contains(renderedJsonDiff, "\n") { | |
| return fmt.Sprintf("jsonencode(%s\n%s%s%s%s\n%s%s)%s", whitespace, formatIndent(indent+1), writeDiffActionSymbol(action, opts), renderedJsonDiff, replace, formatIndent(indent), writeDiffActionSymbol(plans.NoOp, opts), nullSuffix(diff.Action, opts)) | |
| } | |
| return fmt.Sprintf("jsonencode(%s)%s%s", renderedJsonDiff, whitespace, replace) | |
| } |