github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/repl/format.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package repl
     5  
     6  import (
     7  	"fmt"
     8  	"strconv"
     9  	"strings"
    10  
    11  	"github.com/terramate-io/tf/lang/marks"
    12  	"github.com/zclconf/go-cty/cty"
    13  )
    14  
    15  // FormatValue formats a value in a way that resembles Terraform language syntax
    16  // and uses the type conversion functions where necessary to indicate exactly
    17  // what type it is given, so that equality test failures can be quickly
    18  // understood.
    19  func FormatValue(v cty.Value, indent int) string {
    20  	if !v.IsKnown() {
    21  		return "(known after apply)"
    22  	}
    23  	if v.HasMark(marks.Sensitive) {
    24  		return "(sensitive value)"
    25  	}
    26  	if v.IsNull() {
    27  		ty := v.Type()
    28  		switch {
    29  		case ty == cty.DynamicPseudoType:
    30  			return "null"
    31  		case ty == cty.String:
    32  			return "tostring(null)"
    33  		case ty == cty.Number:
    34  			return "tonumber(null)"
    35  		case ty == cty.Bool:
    36  			return "tobool(null)"
    37  		case ty.IsListType():
    38  			return fmt.Sprintf("tolist(null) /* of %s */", ty.ElementType().FriendlyName())
    39  		case ty.IsSetType():
    40  			return fmt.Sprintf("toset(null) /* of %s */", ty.ElementType().FriendlyName())
    41  		case ty.IsMapType():
    42  			return fmt.Sprintf("tomap(null) /* of %s */", ty.ElementType().FriendlyName())
    43  		default:
    44  			return fmt.Sprintf("null /* %s */", ty.FriendlyName())
    45  		}
    46  	}
    47  
    48  	ty := v.Type()
    49  	switch {
    50  	case ty.IsPrimitiveType():
    51  		switch ty {
    52  		case cty.String:
    53  			if formatted, isMultiline := formatMultilineString(v, indent); isMultiline {
    54  				return formatted
    55  			}
    56  			return strconv.Quote(v.AsString())
    57  		case cty.Number:
    58  			bf := v.AsBigFloat()
    59  			return bf.Text('f', -1)
    60  		case cty.Bool:
    61  			if v.True() {
    62  				return "true"
    63  			} else {
    64  				return "false"
    65  			}
    66  		}
    67  	case ty.IsObjectType():
    68  		return formatMappingValue(v, indent)
    69  	case ty.IsTupleType():
    70  		return formatSequenceValue(v, indent)
    71  	case ty.IsListType():
    72  		return fmt.Sprintf("tolist(%s)", formatSequenceValue(v, indent))
    73  	case ty.IsSetType():
    74  		return fmt.Sprintf("toset(%s)", formatSequenceValue(v, indent))
    75  	case ty.IsMapType():
    76  		return fmt.Sprintf("tomap(%s)", formatMappingValue(v, indent))
    77  	}
    78  
    79  	// Should never get here because there are no other types
    80  	return fmt.Sprintf("%#v", v)
    81  }
    82  
    83  func formatMultilineString(v cty.Value, indent int) (string, bool) {
    84  	str := v.AsString()
    85  	lines := strings.Split(str, "\n")
    86  	if len(lines) < 2 {
    87  		return "", false
    88  	}
    89  
    90  	// If the value is indented, we use the indented form of heredoc for readability.
    91  	operator := "<<"
    92  	if indent > 0 {
    93  		operator = "<<-"
    94  	}
    95  
    96  	// Default delimiter is "End Of Text" by convention
    97  	delimiter := "EOT"
    98  
    99  OUTER:
   100  	for {
   101  		// Check if any of the lines are in conflict with the delimiter. The
   102  		// parser allows leading and trailing whitespace, so we must remove it
   103  		// before comparison.
   104  		for _, line := range lines {
   105  			// If the delimiter matches a line, extend it and start again
   106  			if strings.TrimSpace(line) == delimiter {
   107  				delimiter = delimiter + "_"
   108  				continue OUTER
   109  			}
   110  		}
   111  
   112  		// None of the lines match the delimiter, so we're ready
   113  		break
   114  	}
   115  
   116  	// Write the heredoc, with indentation as appropriate.
   117  	var buf strings.Builder
   118  
   119  	buf.WriteString(operator)
   120  	buf.WriteString(delimiter)
   121  	for _, line := range lines {
   122  		buf.WriteByte('\n')
   123  		buf.WriteString(strings.Repeat(" ", indent))
   124  		buf.WriteString(line)
   125  	}
   126  	buf.WriteByte('\n')
   127  	buf.WriteString(strings.Repeat(" ", indent))
   128  	buf.WriteString(delimiter)
   129  
   130  	return buf.String(), true
   131  }
   132  
   133  func formatMappingValue(v cty.Value, indent int) string {
   134  	var buf strings.Builder
   135  	count := 0
   136  	buf.WriteByte('{')
   137  	indent += 2
   138  	for it := v.ElementIterator(); it.Next(); {
   139  		count++
   140  		k, v := it.Element()
   141  		buf.WriteByte('\n')
   142  		buf.WriteString(strings.Repeat(" ", indent))
   143  		buf.WriteString(FormatValue(k, indent))
   144  		buf.WriteString(" = ")
   145  		buf.WriteString(FormatValue(v, indent))
   146  	}
   147  	indent -= 2
   148  	if count > 0 {
   149  		buf.WriteByte('\n')
   150  		buf.WriteString(strings.Repeat(" ", indent))
   151  	}
   152  	buf.WriteByte('}')
   153  	return buf.String()
   154  }
   155  
   156  func formatSequenceValue(v cty.Value, indent int) string {
   157  	var buf strings.Builder
   158  	count := 0
   159  	buf.WriteByte('[')
   160  	indent += 2
   161  	for it := v.ElementIterator(); it.Next(); {
   162  		count++
   163  		_, v := it.Element()
   164  		buf.WriteByte('\n')
   165  		buf.WriteString(strings.Repeat(" ", indent))
   166  		buf.WriteString(FormatValue(v, indent))
   167  		buf.WriteByte(',')
   168  	}
   169  	indent -= 2
   170  	if count > 0 {
   171  		buf.WriteByte('\n')
   172  		buf.WriteString(strings.Repeat(" ", indent))
   173  	}
   174  	buf.WriteByte(']')
   175  	return buf.String()
   176  }