github.com/opentofu/opentofu@v1.7.1/internal/command/jsonformat/computed/renderers/primitive.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package renderers 7 8 import ( 9 "encoding/json" 10 "fmt" 11 "strings" 12 13 "github.com/zclconf/go-cty/cty" 14 15 "github.com/opentofu/opentofu/internal/command/jsonformat/collections" 16 "github.com/opentofu/opentofu/internal/command/jsonformat/computed" 17 "github.com/opentofu/opentofu/internal/command/jsonformat/structured" 18 "github.com/opentofu/opentofu/internal/command/jsonformat/structured/attribute_path" 19 "github.com/opentofu/opentofu/internal/plans" 20 ) 21 22 var _ computed.DiffRenderer = (*primitiveRenderer)(nil) 23 24 func Primitive(before, after interface{}, ctype cty.Type) computed.DiffRenderer { 25 return &primitiveRenderer{ 26 before: before, 27 after: after, 28 ctype: ctype, 29 } 30 } 31 32 type primitiveRenderer struct { 33 NoWarningsRenderer 34 35 before interface{} 36 after interface{} 37 ctype cty.Type 38 } 39 40 func (renderer primitiveRenderer) RenderHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) string { 41 if renderer.ctype == cty.String { 42 return renderer.renderStringDiff(diff, indent, opts) 43 } 44 45 beforeValue := renderPrimitiveValue(renderer.before, renderer.ctype, opts) 46 afterValue := renderPrimitiveValue(renderer.after, renderer.ctype, opts) 47 48 switch diff.Action { 49 case plans.Create: 50 return fmt.Sprintf("%s%s", afterValue, forcesReplacement(diff.Replace, opts)) 51 case plans.Delete: 52 return fmt.Sprintf("%s%s%s", beforeValue, nullSuffix(diff.Action, opts), forcesReplacement(diff.Replace, opts)) 53 case plans.NoOp: 54 return fmt.Sprintf("%s%s", beforeValue, forcesReplacement(diff.Replace, opts)) 55 default: 56 return fmt.Sprintf("%s %s %s%s", beforeValue, opts.Colorize.Color("[yellow]->[reset]"), afterValue, forcesReplacement(diff.Replace, opts)) 57 } 58 } 59 60 func renderPrimitiveValue(value interface{}, t cty.Type, opts computed.RenderHumanOpts) string { 61 if value == nil { 62 return opts.Colorize.Color("[dark_gray]null[reset]") 63 } 64 65 switch { 66 case t == cty.Bool: 67 if value.(bool) { 68 return "true" 69 } 70 return "false" 71 case t == cty.Number: 72 num := value.(json.Number) 73 return num.String() 74 default: 75 panic("unrecognized primitive type: " + t.FriendlyName()) 76 } 77 } 78 79 func (renderer primitiveRenderer) renderStringDiff(diff computed.Diff, indent int, opts computed.RenderHumanOpts) string { 80 81 // We process multiline strings at the end of the switch statement. 82 var lines []string 83 84 switch diff.Action { 85 case plans.Create, plans.NoOp: 86 str := evaluatePrimitiveString(renderer.after, opts) 87 88 if str.Json != nil { 89 if diff.Action == plans.NoOp { 90 return renderer.renderStringDiffAsJson(diff, indent, opts, str, str) 91 } else { 92 return renderer.renderStringDiffAsJson(diff, indent, opts, evaluatedString{}, str) 93 } 94 } 95 96 if !str.IsMultiline { 97 return fmt.Sprintf("%s%s", str.RenderSimple(), forcesReplacement(diff.Replace, opts)) 98 } 99 100 // We are creating a single multiline string, so let's split by the new 101 // line character. While we are doing this, we are going to insert our 102 // indents and make sure each line is formatted correctly. 103 lines = strings.Split(strings.ReplaceAll(str.String, "\n", fmt.Sprintf("\n%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts))), "\n") 104 105 // We now just need to do the same for the first entry in lines, because 106 // we split on the new line characters which won't have been at the 107 // beginning of the first line. 108 lines[0] = fmt.Sprintf("%s%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), lines[0]) 109 case plans.Delete: 110 str := evaluatePrimitiveString(renderer.before, opts) 111 if str.IsNull { 112 // We don't put the null suffix (-> null) here because the final 113 // render or null -> null would look silly. 114 return fmt.Sprintf("%s%s", str.RenderSimple(), forcesReplacement(diff.Replace, opts)) 115 } 116 117 if str.Json != nil { 118 return renderer.renderStringDiffAsJson(diff, indent, opts, str, evaluatedString{}) 119 } 120 121 if !str.IsMultiline { 122 return fmt.Sprintf("%s%s%s", str.RenderSimple(), nullSuffix(diff.Action, opts), forcesReplacement(diff.Replace, opts)) 123 } 124 125 // We are creating a single multiline string, so let's split by the new 126 // line character. While we are doing this, we are going to insert our 127 // indents and make sure each line is formatted correctly. 128 lines = strings.Split(strings.ReplaceAll(str.String, "\n", fmt.Sprintf("\n%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts))), "\n") 129 130 // We now just need to do the same for the first entry in lines, because 131 // we split on the new line characters which won't have been at the 132 // beginning of the first line. 133 lines[0] = fmt.Sprintf("%s%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), lines[0]) 134 default: 135 beforeString := evaluatePrimitiveString(renderer.before, opts) 136 afterString := evaluatePrimitiveString(renderer.after, opts) 137 138 if beforeString.Json != nil && afterString.Json != nil { 139 return renderer.renderStringDiffAsJson(diff, indent, opts, beforeString, afterString) 140 } 141 142 if beforeString.Json != nil || afterString.Json != nil { 143 // This means one of the strings is JSON and one isn't. We're going 144 // to be a little inefficient here, but we can just reuse another 145 // renderer for this so let's keep it simple. 146 return computed.NewDiff( 147 TypeChange( 148 computed.NewDiff(Primitive(renderer.before, nil, cty.String), plans.Delete, false), 149 computed.NewDiff(Primitive(nil, renderer.after, cty.String), plans.Create, false)), 150 diff.Action, 151 diff.Replace).RenderHuman(indent, opts) 152 } 153 154 if !beforeString.IsMultiline && !afterString.IsMultiline { 155 return fmt.Sprintf("%s %s %s%s", beforeString.RenderSimple(), opts.Colorize.Color("[yellow]->[reset]"), afterString.RenderSimple(), forcesReplacement(diff.Replace, opts)) 156 } 157 158 beforeLines := strings.Split(beforeString.String, "\n") 159 afterLines := strings.Split(afterString.String, "\n") 160 161 processIndices := func(beforeIx, afterIx int) { 162 if beforeIx < 0 || beforeIx >= len(beforeLines) { 163 lines = append(lines, fmt.Sprintf("%s%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.Create, opts), afterLines[afterIx])) 164 return 165 } 166 167 if afterIx < 0 || afterIx >= len(afterLines) { 168 lines = append(lines, fmt.Sprintf("%s%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.Delete, opts), beforeLines[beforeIx])) 169 return 170 } 171 172 lines = append(lines, fmt.Sprintf("%s%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), beforeLines[beforeIx])) 173 } 174 isObjType := func(_ string) bool { 175 return false 176 } 177 178 collections.ProcessSlice(beforeLines, afterLines, processIndices, isObjType) 179 } 180 181 // We return early if we find non-multiline strings or JSON strings, so we 182 // know here that we just render the lines slice properly. 183 return fmt.Sprintf("<<-EOT%s\n%s\n%s%sEOT%s", 184 forcesReplacement(diff.Replace, opts), 185 strings.Join(lines, "\n"), 186 formatIndent(indent), 187 writeDiffActionSymbol(plans.NoOp, opts), 188 nullSuffix(diff.Action, opts)) 189 } 190 191 func (renderer primitiveRenderer) renderStringDiffAsJson(diff computed.Diff, indent int, opts computed.RenderHumanOpts, before evaluatedString, after evaluatedString) string { 192 jsonDiff := RendererJsonOpts().Transform(structured.Change{ 193 BeforeExplicit: diff.Action != plans.Create, 194 AfterExplicit: diff.Action != plans.Delete, 195 Before: before.Json, 196 After: after.Json, 197 Unknown: false, 198 BeforeSensitive: false, 199 AfterSensitive: false, 200 ReplacePaths: attribute_path.Empty(false), 201 RelevantAttributes: attribute_path.AlwaysMatcher(), 202 }) 203 204 action := diff.Action 205 206 jsonOpts := opts.Clone() 207 jsonOpts.OverrideNullSuffix = true 208 209 var whitespace, replace string 210 if jsonDiff.Action == plans.NoOp && diff.Action == plans.Update { 211 // Then this means we are rendering a whitespace only change. The JSON 212 // differ will have ignored the whitespace changes so that makes the 213 // diff we are about to print out very confusing without extra 214 // explanation. 215 if diff.Replace { 216 whitespace = " # whitespace changes force replacement" 217 } else { 218 whitespace = " # whitespace changes" 219 } 220 221 // Because we'd be showing no changes otherwise: 222 jsonOpts.ShowUnchangedChildren = true 223 224 // Whitespace changes should not appear as if edited. 225 action = plans.NoOp 226 } else { 227 // We only show the replace suffix if we didn't print something out 228 // about whitespace changes. 229 replace = forcesReplacement(diff.Replace, opts) 230 } 231 232 renderedJsonDiff := jsonDiff.RenderHuman(indent+1, jsonOpts) 233 234 if diff.Action == plans.Create || diff.Action == plans.Delete { 235 // We don't display the '+' or '-' symbols on the JSON diffs, we should 236 // still display the '~' for an update action though. 237 action = plans.NoOp 238 } 239 240 if strings.Contains(renderedJsonDiff, "\n") { 241 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)) 242 } 243 return fmt.Sprintf("jsonencode(%s)%s%s", renderedJsonDiff, whitespace, replace) 244 }