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

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package addrs
     5  
     6  import (
     7  	"fmt"
     8  	"strings"
     9  	"unicode"
    10  
    11  	"github.com/zclconf/go-cty/cty"
    12  	"github.com/zclconf/go-cty/cty/gocty"
    13  )
    14  
    15  // InstanceKey represents the key of an instance within an object that
    16  // contains multiple instances due to using "count" or "for_each" arguments
    17  // in configuration.
    18  //
    19  // IntKey and StringKey are the two implementations of this type. No other
    20  // implementations are allowed. The single instance of an object that _isn't_
    21  // using "count" or "for_each" is represented by NoKey, which is a nil
    22  // InstanceKey.
    23  type InstanceKey interface {
    24  	instanceKeySigil()
    25  	String() string
    26  
    27  	// Value returns the cty.Value of the appropriate type for the InstanceKey
    28  	// value.
    29  	Value() cty.Value
    30  }
    31  
    32  // ParseInstanceKey returns the instance key corresponding to the given value,
    33  // which must be known and non-null.
    34  //
    35  // If an unknown or null value is provided then this function will panic. This
    36  // function is intended to deal with the values that would naturally be found
    37  // in a hcl.TraverseIndex, which (when parsed from source, at least) can never
    38  // contain unknown or null values.
    39  func ParseInstanceKey(key cty.Value) (InstanceKey, error) {
    40  	switch key.Type() {
    41  	case cty.String:
    42  		return StringKey(key.AsString()), nil
    43  	case cty.Number:
    44  		var idx int
    45  		err := gocty.FromCtyValue(key, &idx)
    46  		return IntKey(idx), err
    47  	default:
    48  		return NoKey, fmt.Errorf("either a string or an integer is required")
    49  	}
    50  }
    51  
    52  // NoKey represents the absense of an InstanceKey, for the single instance
    53  // of a configuration object that does not use "count" or "for_each" at all.
    54  var NoKey InstanceKey
    55  
    56  // IntKey is the InstanceKey representation representing integer indices, as
    57  // used when the "count" argument is specified or if for_each is used with
    58  // a sequence type.
    59  type IntKey int
    60  
    61  func (k IntKey) instanceKeySigil() {
    62  }
    63  
    64  func (k IntKey) String() string {
    65  	return fmt.Sprintf("[%d]", int(k))
    66  }
    67  
    68  func (k IntKey) Value() cty.Value {
    69  	return cty.NumberIntVal(int64(k))
    70  }
    71  
    72  // StringKey is the InstanceKey representation representing string indices, as
    73  // used when the "for_each" argument is specified with a map or object type.
    74  type StringKey string
    75  
    76  func (k StringKey) instanceKeySigil() {
    77  }
    78  
    79  func (k StringKey) String() string {
    80  	// We use HCL's quoting syntax here so that we can in principle parse
    81  	// an address constructed by this package as if it were an HCL
    82  	// traversal, even if the string contains HCL's own metacharacters.
    83  	return fmt.Sprintf("[%s]", toHCLQuotedString(string(k)))
    84  }
    85  
    86  func (k StringKey) Value() cty.Value {
    87  	return cty.StringVal(string(k))
    88  }
    89  
    90  // InstanceKeyLess returns true if the first given instance key i should sort
    91  // before the second key j, and false otherwise.
    92  func InstanceKeyLess(i, j InstanceKey) bool {
    93  	iTy := instanceKeyType(i)
    94  	jTy := instanceKeyType(j)
    95  
    96  	switch {
    97  	case i == j:
    98  		return false
    99  	case i == NoKey:
   100  		return true
   101  	case j == NoKey:
   102  		return false
   103  	case iTy != jTy:
   104  		// The ordering here is arbitrary except that we want NoKeyType
   105  		// to sort before the others, so we'll just use the enum values
   106  		// of InstanceKeyType here (where NoKey is zero, sorting before
   107  		// any other).
   108  		return uint32(iTy) < uint32(jTy)
   109  	case iTy == IntKeyType:
   110  		return int(i.(IntKey)) < int(j.(IntKey))
   111  	case iTy == StringKeyType:
   112  		return string(i.(StringKey)) < string(j.(StringKey))
   113  	default:
   114  		// Shouldn't be possible to get down here in practice, since the
   115  		// above is exhaustive.
   116  		return false
   117  	}
   118  }
   119  
   120  func instanceKeyType(k InstanceKey) InstanceKeyType {
   121  	if _, ok := k.(StringKey); ok {
   122  		return StringKeyType
   123  	}
   124  	if _, ok := k.(IntKey); ok {
   125  		return IntKeyType
   126  	}
   127  	return NoKeyType
   128  }
   129  
   130  // InstanceKeyType represents the different types of instance key that are
   131  // supported. Usually it is sufficient to simply type-assert an InstanceKey
   132  // value to either IntKey or StringKey, but this type and its values can be
   133  // used to represent the types themselves, rather than specific values
   134  // of those types.
   135  type InstanceKeyType rune
   136  
   137  const (
   138  	NoKeyType     InstanceKeyType = 0
   139  	IntKeyType    InstanceKeyType = 'I'
   140  	StringKeyType InstanceKeyType = 'S'
   141  )
   142  
   143  // toHCLQuotedString is a helper which formats the given string in a way that
   144  // HCL's expression parser would treat as a quoted string template.
   145  //
   146  // This includes:
   147  //   - Adding quote marks at the start and the end.
   148  //   - Using backslash escapes as needed for characters that cannot be represented directly.
   149  //   - Escaping anything that would be treated as a template interpolation or control sequence.
   150  func toHCLQuotedString(s string) string {
   151  	// This is an adaptation of a similar function inside the hclwrite package,
   152  	// inlined here because hclwrite's version generates HCL tokens but we
   153  	// only need normal strings.
   154  	if len(s) == 0 {
   155  		return `""`
   156  	}
   157  	var buf strings.Builder
   158  	buf.WriteByte('"')
   159  	for i, r := range s {
   160  		switch r {
   161  		case '\n':
   162  			buf.WriteString(`\n`)
   163  		case '\r':
   164  			buf.WriteString(`\r`)
   165  		case '\t':
   166  			buf.WriteString(`\t`)
   167  		case '"':
   168  			buf.WriteString(`\"`)
   169  		case '\\':
   170  			buf.WriteString(`\\`)
   171  		case '$', '%':
   172  			buf.WriteRune(r)
   173  			remain := s[i+1:]
   174  			if len(remain) > 0 && remain[0] == '{' {
   175  				// Double up our template introducer symbol to escape it.
   176  				buf.WriteRune(r)
   177  			}
   178  		default:
   179  			if !unicode.IsPrint(r) {
   180  				var fmted string
   181  				if r < 65536 {
   182  					fmted = fmt.Sprintf("\\u%04x", r)
   183  				} else {
   184  					fmted = fmt.Sprintf("\\U%08x", r)
   185  				}
   186  				buf.WriteString(fmted)
   187  			} else {
   188  				buf.WriteRune(r)
   189  			}
   190  		}
   191  	}
   192  	buf.WriteByte('"')
   193  	return buf.String()
   194  }