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 }