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  }