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

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package repl
     5  
     6  import (
     7  	"fmt"
     8  	"sort"
     9  	"strings"
    10  
    11  	"github.com/zclconf/go-cty/cty"
    12  
    13  	"github.com/hashicorp/hcl/v2"
    14  	"github.com/hashicorp/hcl/v2/hclsyntax"
    15  	"github.com/terramate-io/tf/lang"
    16  	"github.com/terramate-io/tf/lang/marks"
    17  	"github.com/terramate-io/tf/lang/types"
    18  	"github.com/terramate-io/tf/tfdiags"
    19  )
    20  
    21  // Session represents the state for a single REPL session.
    22  type Session struct {
    23  	// Scope is the evaluation scope where expressions will be evaluated.
    24  	Scope *lang.Scope
    25  }
    26  
    27  // Handle handles a single line of input from the REPL.
    28  //
    29  // This is a stateful operation if a command is given (such as setting
    30  // a variable). This function should not be called in parallel.
    31  //
    32  // The return value is the output and the error to show.
    33  func (s *Session) Handle(line string) (string, bool, tfdiags.Diagnostics) {
    34  	switch {
    35  	case strings.TrimSpace(line) == "":
    36  		return "", false, nil
    37  	case strings.TrimSpace(line) == "exit":
    38  		return "", true, nil
    39  	case strings.TrimSpace(line) == "help":
    40  		ret, diags := s.handleHelp()
    41  		return ret, false, diags
    42  	default:
    43  		ret, diags := s.handleEval(line)
    44  		return ret, false, diags
    45  	}
    46  }
    47  
    48  func (s *Session) handleEval(line string) (string, tfdiags.Diagnostics) {
    49  	var diags tfdiags.Diagnostics
    50  
    51  	// Parse the given line as an expression
    52  	expr, parseDiags := hclsyntax.ParseExpression([]byte(line), "<console-input>", hcl.Pos{Line: 1, Column: 1})
    53  	diags = diags.Append(parseDiags)
    54  	if parseDiags.HasErrors() {
    55  		return "", diags
    56  	}
    57  
    58  	val, valDiags := s.Scope.EvalExpr(expr, cty.DynamicPseudoType)
    59  	diags = diags.Append(valDiags)
    60  	if valDiags.HasErrors() {
    61  		return "", diags
    62  	}
    63  
    64  	// The TypeType mark is used only by the console-only `type` function, in
    65  	// order to smuggle the type of a given value back here. We can then
    66  	// display a representation of the type directly.
    67  	if marks.Contains(val, marks.TypeType) {
    68  		val, _ = val.UnmarkDeep()
    69  
    70  		valType := val.Type()
    71  		switch {
    72  		case valType.Equals(types.TypeType):
    73  			// An encapsulated type value, which should be displayed directly.
    74  			valType := val.EncapsulatedValue().(*cty.Type)
    75  			return typeString(*valType), diags
    76  		default:
    77  			diags = diags.Append(tfdiags.Sourceless(
    78  				tfdiags.Error,
    79  				"Invalid use of type function",
    80  				"The console-only \"type\" function cannot be used as part of an expression.",
    81  			))
    82  			return "", diags
    83  		}
    84  	}
    85  
    86  	return FormatValue(val, 0), diags
    87  }
    88  
    89  func (s *Session) handleHelp() (string, tfdiags.Diagnostics) {
    90  	text := `
    91  The Terraform console allows you to experiment with Terraform interpolations.
    92  You may access resources in the state (if you have one) just as you would
    93  from a configuration. For example: "aws_instance.foo.id" would evaluate
    94  to the ID of "aws_instance.foo" if it exists in your state.
    95  
    96  Type in the interpolation to test and hit <enter> to see the result.
    97  
    98  To exit the console, type "exit" and hit <enter>, or use Control-C or
    99  Control-D.
   100  `
   101  
   102  	return strings.TrimSpace(text), nil
   103  }
   104  
   105  // Modified copy of TypeString from go-cty:
   106  // https://github.com/zclconf/go-cty-debug/blob/master/ctydebug/type_string.go
   107  //
   108  // TypeString returns a string representation of a given type that is
   109  // reminiscent of Go syntax calling into the cty package but is mainly
   110  // intended for easy human inspection of values in tests, debug output, etc.
   111  //
   112  // The resulting string will include newlines and indentation in order to
   113  // increase the readability of complex structures. It always ends with a
   114  // newline, so you can print this result directly to your output.
   115  func typeString(ty cty.Type) string {
   116  	var b strings.Builder
   117  	writeType(ty, &b, 0)
   118  	return b.String()
   119  }
   120  
   121  func writeType(ty cty.Type, b *strings.Builder, indent int) {
   122  	switch {
   123  	case ty == cty.NilType:
   124  		b.WriteString("nil")
   125  		return
   126  	case ty.IsObjectType():
   127  		atys := ty.AttributeTypes()
   128  		if len(atys) == 0 {
   129  			b.WriteString("object({})")
   130  			return
   131  		}
   132  		attrNames := make([]string, 0, len(atys))
   133  		for name := range atys {
   134  			attrNames = append(attrNames, name)
   135  		}
   136  		sort.Strings(attrNames)
   137  		b.WriteString("object({\n")
   138  		indent++
   139  		for _, name := range attrNames {
   140  			aty := atys[name]
   141  			b.WriteString(indentSpaces(indent))
   142  			fmt.Fprintf(b, "%s: ", name)
   143  			writeType(aty, b, indent)
   144  			b.WriteString(",\n")
   145  		}
   146  		indent--
   147  		b.WriteString(indentSpaces(indent))
   148  		b.WriteString("})")
   149  	case ty.IsTupleType():
   150  		etys := ty.TupleElementTypes()
   151  		if len(etys) == 0 {
   152  			b.WriteString("tuple([])")
   153  			return
   154  		}
   155  		b.WriteString("tuple([\n")
   156  		indent++
   157  		for _, ety := range etys {
   158  			b.WriteString(indentSpaces(indent))
   159  			writeType(ety, b, indent)
   160  			b.WriteString(",\n")
   161  		}
   162  		indent--
   163  		b.WriteString(indentSpaces(indent))
   164  		b.WriteString("])")
   165  	case ty.IsCollectionType():
   166  		ety := ty.ElementType()
   167  		switch {
   168  		case ty.IsListType():
   169  			b.WriteString("list(")
   170  		case ty.IsMapType():
   171  			b.WriteString("map(")
   172  		case ty.IsSetType():
   173  			b.WriteString("set(")
   174  		default:
   175  			// At the time of writing there are no other collection types,
   176  			// but we'll be robust here and just pass through the GoString
   177  			// of anything we don't recognize.
   178  			b.WriteString(ty.FriendlyName())
   179  			return
   180  		}
   181  		// Because object and tuple types render split over multiple
   182  		// lines, a collection type container around them can end up
   183  		// being hard to see when scanning, so we'll generate some extra
   184  		// indentation to make a collection of structural type more visually
   185  		// distinct from the structural type alone.
   186  		complexElem := ety.IsObjectType() || ety.IsTupleType()
   187  		if complexElem {
   188  			indent++
   189  			b.WriteString("\n")
   190  			b.WriteString(indentSpaces(indent))
   191  		}
   192  		writeType(ty.ElementType(), b, indent)
   193  		if complexElem {
   194  			indent--
   195  			b.WriteString(",\n")
   196  			b.WriteString(indentSpaces(indent))
   197  		}
   198  		b.WriteString(")")
   199  	default:
   200  		// For any other type we'll just use its GoString and assume it'll
   201  		// follow the usual GoString conventions.
   202  		b.WriteString(ty.FriendlyName())
   203  	}
   204  }
   205  
   206  func indentSpaces(level int) string {
   207  	return strings.Repeat("    ", level)
   208  }