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