Files
opentf/internal/repl/format.go
Martin Atkins 6cc3fc6a07 repl: FormatValue factor out printing of null values
This value was too long for our function length lint rule, and factoring
out the printing of null values makes this more balanced with how we're
already handling unknown values and sensitive values so that the main
body of FormatValue is focused on the normal value printing case.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2025-01-06 08:38:44 -08:00

182 lines
4.4 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package repl
import (
"fmt"
"strconv"
"strings"
"github.com/opentofu/opentofu/internal/lang/marks"
"github.com/zclconf/go-cty/cty"
)
// FormatValue formats a value in a way that resembles OpenTofu language syntax
// and uses the type conversion functions where necessary to indicate exactly
// what type it is given, so that equality test failures can be quickly
// understood.
func FormatValue(v cty.Value, indent int) string {
if !v.IsKnown() {
return "(known after apply)"
}
if v.HasMark(marks.Sensitive) {
return "(sensitive value)"
}
if v.IsNull() {
return formatNullValue(v.Type())
}
ty := v.Type()
switch {
case ty.IsPrimitiveType():
switch ty {
case cty.String:
if formatted, isMultiline := formatMultilineString(v, indent); isMultiline {
return formatted
}
return strconv.Quote(v.AsString())
case cty.Number:
bf := v.AsBigFloat()
return bf.Text('f', -1)
case cty.Bool:
if v.True() {
return "true"
} else {
return "false"
}
}
case ty.IsObjectType():
return formatMappingValue(v, indent)
case ty.IsTupleType():
return formatSequenceValue(v, indent)
case ty.IsListType():
return fmt.Sprintf("tolist(%s)", formatSequenceValue(v, indent))
case ty.IsSetType():
return fmt.Sprintf("toset(%s)", formatSequenceValue(v, indent))
case ty.IsMapType():
return fmt.Sprintf("tomap(%s)", formatMappingValue(v, indent))
}
// Should never get here because there are no other types
return fmt.Sprintf("%#v", v)
}
func formatNullValue(ty cty.Type) string {
switch {
case ty == cty.DynamicPseudoType:
return "null"
case ty == cty.String:
return "tostring(null)"
case ty == cty.Number:
return "tonumber(null)"
case ty == cty.Bool:
return "tobool(null)"
case ty.IsListType():
return fmt.Sprintf("tolist(null) /* of %s */", ty.ElementType().FriendlyName())
case ty.IsSetType():
return fmt.Sprintf("toset(null) /* of %s */", ty.ElementType().FriendlyName())
case ty.IsMapType():
return fmt.Sprintf("tomap(null) /* of %s */", ty.ElementType().FriendlyName())
default:
return fmt.Sprintf("null /* %s */", ty.FriendlyName())
}
}
func formatMultilineString(v cty.Value, indent int) (string, bool) {
str := v.AsString()
lines := strings.Split(str, "\n")
if len(lines) < 2 {
return "", false
}
// If the value is indented, we use the indented form of heredoc for readability.
operator := "<<"
if indent > 0 {
operator = "<<-"
}
// Default delimiter is "End Of Text" by convention
delimiter := "EOT"
OUTER:
for {
// Check if any of the lines are in conflict with the delimiter. The
// parser allows leading and trailing whitespace, so we must remove it
// before comparison.
for _, line := range lines {
// If the delimiter matches a line, extend it and start again
if strings.TrimSpace(line) == delimiter {
delimiter = delimiter + "_"
continue OUTER
}
}
// None of the lines match the delimiter, so we're ready
break
}
// Write the heredoc, with indentation as appropriate.
var buf strings.Builder
buf.WriteString(operator)
buf.WriteString(delimiter)
for _, line := range lines {
buf.WriteByte('\n')
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString(line)
}
buf.WriteByte('\n')
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString(delimiter)
return buf.String(), true
}
func formatMappingValue(v cty.Value, indent int) string {
var buf strings.Builder
count := 0
buf.WriteByte('{')
indent += 2
for it := v.ElementIterator(); it.Next(); {
count++
k, v := it.Element()
buf.WriteByte('\n')
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString(FormatValue(k, indent))
buf.WriteString(" = ")
buf.WriteString(FormatValue(v, indent))
}
indent -= 2
if count > 0 {
buf.WriteByte('\n')
buf.WriteString(strings.Repeat(" ", indent))
}
buf.WriteByte('}')
return buf.String()
}
func formatSequenceValue(v cty.Value, indent int) string {
var buf strings.Builder
count := 0
buf.WriteByte('[')
indent += 2
for it := v.ElementIterator(); it.Next(); {
count++
_, v := it.Element()
buf.WriteByte('\n')
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString(FormatValue(v, indent))
buf.WriteByte(',')
}
indent -= 2
if count > 0 {
buf.WriteByte('\n')
buf.WriteString(strings.Repeat(" ", indent))
}
buf.WriteByte(']')
return buf.String()
}