github.com/kevinklinger/open_terraform@v1.3.6/noninternal/repl/session.go (about)

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