github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/repl/format.go (about)

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