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  }