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 }