├── .github └── FUNDING.yml ├── .travis.sh ├── .travis.yml ├── CHANGELOG.md ├── COMPATIBILITY.md ├── LICENSE ├── README.md ├── cty ├── capsule.go ├── capsule_ops.go ├── capsule_test.go ├── collection.go ├── convert │ ├── compare_types.go │ ├── compare_types_test.go │ ├── conversion.go │ ├── conversion_capsule.go │ ├── conversion_capsule_test.go │ ├── conversion_collection.go │ ├── conversion_dynamic.go │ ├── conversion_object.go │ ├── conversion_primitive.go │ ├── conversion_tuple.go │ ├── doc.go │ ├── mismatch_msg.go │ ├── mismatch_msg_test.go │ ├── public.go │ ├── public_test.go │ ├── sort_types.go │ ├── sort_types_test.go │ ├── unify.go │ └── unify_test.go ├── ctystrings │ ├── doc.go │ ├── normalize.go │ ├── prefix.go │ └── prefix_test.go ├── doc.go ├── element_iterator.go ├── error.go ├── function │ ├── argument.go │ ├── doc.go │ ├── error.go │ ├── function.go │ ├── function_test.go │ ├── stdlib │ │ ├── bool.go │ │ ├── bool_test.go │ │ ├── bytes.go │ │ ├── bytes_test.go │ │ ├── collection.go │ │ ├── collection_test.go │ │ ├── conversion.go │ │ ├── conversion_test.go │ │ ├── csv.go │ │ ├── csv_test.go │ │ ├── datetime.go │ │ ├── datetime_rfc3339.go │ │ ├── datetime_test.go │ │ ├── doc.go │ │ ├── format.go │ │ ├── format_fsm.go │ │ ├── format_fsm.rl │ │ ├── format_test.go │ │ ├── general.go │ │ ├── general_test.go │ │ ├── json.go │ │ ├── json_test.go │ │ ├── number.go │ │ ├── number_test.go │ │ ├── regexp.go │ │ ├── regexp_test.go │ │ ├── sequence.go │ │ ├── sequence_test.go │ │ ├── set.go │ │ ├── set_test.go │ │ ├── string.go │ │ ├── string_replace.go │ │ ├── string_replace_test.go │ │ ├── string_test.go │ │ └── testdata │ │ │ ├── bare.tmpl │ │ │ ├── func.tmpl │ │ │ ├── hello.tmpl │ │ │ ├── hello.txt │ │ │ ├── icon.png │ │ │ ├── list.tmpl │ │ │ └── recursive.tmpl │ ├── unpredictable.go │ └── unpredictable_test.go ├── gocty │ ├── doc.go │ ├── helpers.go │ ├── in.go │ ├── in_test.go │ ├── out.go │ ├── out_test.go │ ├── type_implied.go │ └── type_implied_test.go ├── helper.go ├── json.go ├── json │ ├── doc.go │ ├── marshal.go │ ├── simple.go │ ├── simple_test.go │ ├── type.go │ ├── type_implied.go │ ├── type_implied_test.go │ ├── unmarshal.go │ ├── value.go │ └── value_test.go ├── json_test.go ├── list_type.go ├── map_type.go ├── marks.go ├── marks_test.go ├── msgpack │ ├── doc.go │ ├── dynamic.go │ ├── infinity.go │ ├── marshal.go │ ├── roundtrip_test.go │ ├── type_implied.go │ ├── type_implied_test.go │ ├── unknown.go │ └── unmarshal.go ├── null.go ├── object_type.go ├── object_type_test.go ├── path.go ├── path_set.go ├── path_set_test.go ├── path_test.go ├── primitive_type.go ├── primitive_type_test.go ├── set │ ├── iterator.go │ ├── ops.go │ ├── ops_test.go │ ├── rules.go │ ├── rules_test.go │ └── set.go ├── set_helper.go ├── set_internals.go ├── set_internals_test.go ├── set_type.go ├── set_type_test.go ├── tuple_type.go ├── tuple_type_test.go ├── type.go ├── type_conform.go ├── type_conform_test.go ├── type_test.go ├── unknown.go ├── unknown_as_null.go ├── unknown_as_null_test.go ├── unknown_refinement.go ├── unknown_refinement_test.go ├── value.go ├── value_init.go ├── value_init_test.go ├── value_ops.go ├── value_ops_test.go ├── value_range.go ├── walk.go └── walk_test.go ├── docs ├── capsule-type-operations.md ├── concepts.md ├── convert.md ├── functions.md ├── gocty.md ├── json.md ├── marks.md ├── refinements.md └── types.md ├── go.mod └── go.sum /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: apparentlymart 2 | -------------------------------------------------------------------------------- /.travis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | echo "" > coverage.txt 5 | 6 | for d in $(go list ./... | grep -v vendor); do 7 | go test -coverprofile=profile.out -covermode=atomic $d 8 | if [ -f profile.out ]; then 9 | cat profile.out >> coverage.txt 10 | rm profile.out 11 | fi 12 | done 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.15.x 5 | - 1.16.x 6 | - tip 7 | 8 | before_install: 9 | - go get -t -v ./... 10 | 11 | script: 12 | - ./.travis.sh 13 | -------------------------------------------------------------------------------- /COMPATIBILITY.md: -------------------------------------------------------------------------------- 1 | # `cty` backward-compatibility policy 2 | 3 | This library includes a number of behaviors that aim to support "best effort" 4 | partial evaluation in the presence of wholly- or partially-unknown inputs. 5 | Over time we've improved the accuracy of those analyses, but doing so changes 6 | the specific results returned by certain operations. 7 | 8 | This document aims to describe what sorts of changes are allowed in new minor 9 | releases and how those changes might affect the behavior of dependents after 10 | upgrading. 11 | 12 | Where possible we'll avoid making changes like these in _patch_ releases, which 13 | focus instead only on correcting incorrect behavior. An exception would be if 14 | a minor release introduced an incorrect behavior and then a patch release 15 | repaired it to either restore the previous correct behavior or implement a new 16 | compromise correct behavior. 17 | 18 | ## Unknown Values can become "more known" 19 | 20 | The most significant policy is that any operation that was previously returning 21 | an unknown value may return either a known value or a _more refined_ unknown 22 | value in later releases, as long as the new result is a subset of the range 23 | of the previous result. 24 | 25 | When using only the _operation methods_ and functionality derived from them, 26 | `cty` will typically handle these deductions automatically and return the most 27 | specific result it is able to. In those cases we expect that these changes will 28 | be seen as an improvement for end-users, and not require significant changes 29 | to calling applications to pass on those benefits. 30 | 31 | When working with _integration methods_ (those which return results using 32 | "normal" Go types rather than `cty.Value`) these changes can be more sigificant, 33 | because applications can therefore observe the differences more readily. 34 | For example, if an unknown value is replaced with a known value of the same 35 | type then `Value.IsKnown` will begin returning `true` where it previously 36 | returned `false`. Applications should be designed to avoid depending on 37 | specific implementation details like these and instead aim to be more general 38 | to handle both known and unknown values. 39 | 40 | A specific sensitive area for compatibility is the `Value.RawEquals` method, 41 | which is sensitive to all of the possible variations in values. Applications 42 | should not use this method for normal application code to avoid exposing 43 | implementation details to end-users, but might use it to assert exact expected 44 | results in unit tests. Such test cases may begin failing after upgrading, and 45 | application developers should carefully consider whether the new results conform 46 | to these rules and update the tests to match as part of their upgrade if so. If 47 | the changed result seems _not_ to conform to these rules then that might be a 48 | bug; please report it! 49 | 50 | ## Error situations may begin succeeding 51 | 52 | Over time the valid inputs or other constraints on functionality might be 53 | loosened to support new capabilities. Any operation or function that returned 54 | an error in a previous release can begin succeeding with any valid result in 55 | a new release. 56 | 57 | ## Error message text might change 58 | 59 | This library aims to generate good, actionable error messages for user-facing 60 | problems and to give sufficient information to a calling application to generate 61 | its own high-quality error messages in situations where `cty` is not directly 62 | "talking to" an end-user. 63 | 64 | This means that in later releases the exact text of error messages in certain 65 | situations may change, typically to add additional context or increase 66 | precision. 67 | 68 | If a function is documented as returning a particular error type in a certain 69 | situation then that should be preserved in future releases, but if there is 70 | no explicit documentation then calling applications should not depend on the 71 | dynamic type of any `error` result, or should at least do so cautiously with 72 | a fallback to a general error handler. 73 | 74 | ## Passing on changes to Go standard library 75 | 76 | Some parts of `cty` are wrappers around functionality implemented in the Go 77 | standard library. If the underlying packages change in newer versions of Go 78 | then we may or may not pass on the change through the `cty` API, depending on 79 | the circumstances. 80 | 81 | A specific notable example is Unicode support: this library depends on various 82 | Unicode algorithms and data tables indirectly through its dependencies, 83 | including some in the Go standard library, and so its exact treatment of strings 84 | is likely to vary between releases as the Unicode standard grows. We aim to 85 | follow the version of Unicode supported in the latest version of the Go standard 86 | library, although we may lag behind slightly after new Go releases due to the 87 | need to update other libraries that implement other parts of the Unicode 88 | specifications. 89 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2018 Martin Atkins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cty 2 | 3 | `cty` (pronounced "see-tie", emoji: :eyes: :necktie:, 4 | [IPA](https://en.wikipedia.org/wiki/International_Phonetic_Alphabet): /si'tʰaɪ/) 5 | is a dynamic type system for applications written 6 | in Go that need to represent user-supplied values without losing type 7 | information. The primary intended use is for implementing configuration 8 | languages, but other uses may be possible too. 9 | 10 | One could think of `cty` as being the reflection API for a language that 11 | doesn't exist, or that doesn't exist _yet_. It provides a set of value types 12 | and an API for working with values of that type. 13 | 14 | Fundamentally what `cty` provides is equivalent to an `interface{}` with some 15 | dynamic type information attached, but `cty` encapsulates this to ensure that 16 | invariants are preserved and to provide a more convenient API. 17 | 18 | As well as primitive types, basic collection types (lists, maps and sets) and 19 | structural types (object, tuple), the `cty` type and value system has some 20 | additional, optional features that may be useful to certain applications: 21 | 22 | * Representation of "unknown" values, which serve as a typed placeholder for 23 | a value that has yet to be determined. This can be a useful building-block 24 | for a type checker. Unknown values support all of the same operations as 25 | known values of their type, but the result will often itself be unknown. 26 | 27 | * Representation of values whose _types_ aren't even known yet. This can 28 | represent, for example, the result of a JSON-decoding function before the 29 | JSON data is known. 30 | 31 | Along with the type system itself, a number of utility packages are provided 32 | that build on the basics to help integrate `cty` into calling applications. 33 | For example, `cty` values can be automatically converted to other types, 34 | converted to and from native Go data structures, or serialized as JSON. 35 | 36 | For more details, see the following documentation: 37 | 38 | * [Concepts](./docs/concepts.md) 39 | * [Full Description of the `cty` Types](./docs/types.md) 40 | * [API Reference](https://godoc.org/github.com/zclconf/go-cty/cty) (godoc) 41 | * [Conversion between `cty` types](./docs/convert.md) 42 | * [Conversion to and from native Go values](./docs/gocty.md) 43 | * [JSON serialization](./docs/json.md) 44 | * [`cty` Functions system](./docs/functions.md) 45 | * [Compatibility Policy for future Minor Releases](./COMPATIBILITY.md): please 46 | review this before using `cty` in your application to avoid depending on 47 | implementation details that may change. 48 | 49 | --- 50 | 51 | ## License 52 | 53 | Copyright 2017 Martin Atkins 54 | 55 | Permission is hereby granted, free of charge, to any person obtaining a copy 56 | of this software and associated documentation files (the "Software"), to deal 57 | in the Software without restriction, including without limitation the rights 58 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 59 | copies of the Software, and to permit persons to whom the Software is 60 | furnished to do so, subject to the following conditions: 61 | 62 | The above copyright notice and this permission notice shall be included in all 63 | copies or substantial portions of the Software. 64 | 65 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 66 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 67 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 68 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 69 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 70 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 71 | SOFTWARE. 72 | -------------------------------------------------------------------------------- /cty/capsule.go: -------------------------------------------------------------------------------- 1 | package cty 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | type capsuleType struct { 9 | typeImplSigil 10 | Name string 11 | GoType reflect.Type 12 | Ops *CapsuleOps 13 | } 14 | 15 | func (t *capsuleType) Equals(other Type) bool { 16 | if otherP, ok := other.typeImpl.(*capsuleType); ok { 17 | // capsule types compare by pointer identity 18 | return otherP == t 19 | } 20 | return false 21 | } 22 | 23 | func (t *capsuleType) FriendlyName(mode friendlyTypeNameMode) string { 24 | return t.Name 25 | } 26 | 27 | func (t *capsuleType) GoString() string { 28 | impl := t.Ops.TypeGoString 29 | if impl == nil { 30 | // To get a useful representation of our native type requires some 31 | // shenanigans. 32 | victimVal := reflect.Zero(t.GoType) 33 | if t.Ops == noCapsuleOps { 34 | return fmt.Sprintf("cty.Capsule(%q, reflect.TypeOf(%#v))", t.Name, victimVal.Interface()) 35 | } else { 36 | // Including the operations in the output will make this _very_ long, 37 | // so in practice any capsule type with ops ought to provide a 38 | // TypeGoString function to override this with something more 39 | // reasonable. 40 | return fmt.Sprintf("cty.CapsuleWithOps(%q, reflect.TypeOf(%#v), %#v)", t.Name, victimVal.Interface(), t.Ops) 41 | } 42 | } 43 | return impl(t.GoType) 44 | } 45 | 46 | // Capsule creates a new Capsule type. 47 | // 48 | // A Capsule type is a special type that can be used to transport arbitrary 49 | // Go native values of a given type through the cty type system. A language 50 | // that uses cty as its type system might, for example, provide functions 51 | // that return capsule-typed values and then other functions that operate 52 | // on those values. 53 | // 54 | // From cty's perspective, Capsule types have a few interesting characteristics, 55 | // described in the following paragraphs. 56 | // 57 | // Each capsule type has an associated Go native type that it is able to 58 | // transport. Capsule types compare by identity, so each call to the 59 | // Capsule function creates an entirely-distinct cty Type, even if two calls 60 | // use the same native type. 61 | // 62 | // Each capsule-typed value contains a pointer to a value of the given native 63 | // type. A capsule-typed value by default supports no operations except 64 | // equality, and equality is implemented by pointer identity of the 65 | // encapsulated pointer. A capsule type can optionally have its own 66 | // implementations of certain operations if it is created with CapsuleWithOps 67 | // instead of Capsule. 68 | // 69 | // The given name is used as the new type's "friendly name". This can be any 70 | // string in principle, but will usually be a short, all-lowercase name aimed 71 | // at users of the embedding language (i.e. not mention Go-specific details) 72 | // and will ideally not create ambiguity with any predefined cty type. 73 | // 74 | // Capsule types are never introduced by any standard cty operation, so a 75 | // calling application opts in to including them within its own type system 76 | // by creating them and introducing them via its own functions. At that point, 77 | // the application is responsible for dealing with any capsule-typed values 78 | // that might be returned. 79 | func Capsule(name string, nativeType reflect.Type) Type { 80 | return Type{ 81 | &capsuleType{ 82 | Name: name, 83 | GoType: nativeType, 84 | Ops: noCapsuleOps, 85 | }, 86 | } 87 | } 88 | 89 | // CapsuleWithOps is like Capsule except the caller may provide an object 90 | // representing some overloaded operation implementations to associate with 91 | // the given capsule type. 92 | // 93 | // All of the other caveats and restrictions for capsule types still apply, but 94 | // overloaded operations can potentially help a capsule type participate better 95 | // in cty operations. 96 | func CapsuleWithOps(name string, nativeType reflect.Type, ops *CapsuleOps) Type { 97 | // Copy the operations to make sure the caller can't modify them after 98 | // we're constructed. 99 | ourOps := *ops 100 | ourOps.assertValid() 101 | 102 | return Type{ 103 | &capsuleType{ 104 | Name: name, 105 | GoType: nativeType, 106 | Ops: &ourOps, 107 | }, 108 | } 109 | } 110 | 111 | // IsCapsuleType returns true if this type is a capsule type, as created 112 | // by cty.Capsule . 113 | func (t Type) IsCapsuleType() bool { 114 | _, ok := t.typeImpl.(*capsuleType) 115 | return ok 116 | } 117 | 118 | // EncapsulatedType returns the encapsulated native type of a capsule type, 119 | // or panics if the receiver is not a Capsule type. 120 | // 121 | // Is IsCapsuleType to determine if this method is safe to call. 122 | func (t Type) EncapsulatedType() reflect.Type { 123 | impl, ok := t.typeImpl.(*capsuleType) 124 | if !ok { 125 | panic("not a capsule type") 126 | } 127 | return impl.GoType 128 | } 129 | -------------------------------------------------------------------------------- /cty/capsule_test.go: -------------------------------------------------------------------------------- 1 | package cty 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/google/go-cmp/cmp" 9 | ) 10 | 11 | type capsuleTestType1Native struct { 12 | name string 13 | } 14 | 15 | type capsuleTestType2Native struct { 16 | name string 17 | } 18 | 19 | var capsuleTestType1 = Capsule( 20 | "capsule test type 1", 21 | reflect.TypeOf(capsuleTestType1Native{}), 22 | ) 23 | 24 | var capsuleTestType2 = Capsule( 25 | "capsule test type 2", 26 | reflect.TypeOf(capsuleTestType2Native{}), 27 | ) 28 | 29 | func TestCapsuleWithOps(t *testing.T) { 30 | var i = 0 31 | var i2 = 0 32 | var i3 = 1 33 | t.Run("with ops", func(t *testing.T) { 34 | ty := CapsuleWithOps("with ops", reflect.TypeOf(0), &CapsuleOps{ 35 | GoString: func(v interface{}) string { 36 | iPtr := v.(*int) 37 | return fmt.Sprintf("test.WithOpsVal(%#v)", *iPtr) 38 | }, 39 | TypeGoString: func(ty reflect.Type) string { 40 | return fmt.Sprintf("test.WithOps(%s)", ty) 41 | }, 42 | Equals: func(a, b interface{}) Value { 43 | aPtr := a.(*int) 44 | bPtr := b.(*int) 45 | return BoolVal(*aPtr == *bPtr) 46 | }, 47 | RawEquals: func(a, b interface{}) bool { 48 | aPtr := a.(*int) 49 | bPtr := b.(*int) 50 | return *aPtr == *bPtr 51 | }, 52 | }) 53 | v := CapsuleVal(ty, &i) 54 | v2 := CapsuleVal(ty, &i2) 55 | v3 := CapsuleVal(ty, &i3) 56 | 57 | got := map[string]interface{}{} 58 | got["GoString"] = v.GoString() 59 | got["TypeGoString"] = ty.GoString() 60 | got["Equals.Yes"] = v.Equals(v2) 61 | got["Equals.No"] = v.Equals(v3) 62 | 63 | want := map[string]interface{}{ 64 | "GoString": "test.WithOpsVal(0)", 65 | "TypeGoString": "test.WithOps(int)", 66 | "Equals.Yes": True, 67 | "Equals.No": False, 68 | } 69 | 70 | valCmp := cmp.Comparer(Value.RawEquals) 71 | if diff := cmp.Diff(want, got, valCmp); diff != "" { 72 | t.Errorf("wrong results\n%s", diff) 73 | } 74 | }) 75 | t.Run("without ops", func(t *testing.T) { 76 | ty := Capsule("without ops", reflect.TypeOf(0)) 77 | v := CapsuleVal(ty, &i) 78 | v2 := CapsuleVal(ty, &i2) 79 | 80 | got := map[string]interface{}{} 81 | got["GoString"] = v.GoString() 82 | got["TypeGoString"] = ty.GoString() 83 | got["Equals"] = v.Equals(v2) 84 | got["RawEquals"] = v.RawEquals(v2) 85 | 86 | want := map[string]interface{}{ 87 | "GoString": fmt.Sprintf(`cty.CapsuleVal(cty.Capsule("without ops", reflect.TypeOf(0)), (*int)(0x%x))`, &i), 88 | "TypeGoString": `cty.Capsule("without ops", reflect.TypeOf(0))`, 89 | "Equals": False, 90 | "RawEquals": false, 91 | } 92 | 93 | valCmp := cmp.Comparer(Value.RawEquals) 94 | if diff := cmp.Diff(want, got, valCmp); diff != "" { 95 | t.Errorf("wrong results\n%s", diff) 96 | } 97 | }) 98 | 99 | } 100 | 101 | func TestCapsuleExtensionData(t *testing.T) { 102 | ty := CapsuleWithOps("with extension data", reflect.TypeOf(0), &CapsuleOps{ 103 | ExtensionData: func(key interface{}) interface{} { 104 | switch key { 105 | // Note that this is a bad example of a key, just using a plain 106 | // string for easier testing. Real-world extension keys should 107 | // be named types belonging to a package in the application that 108 | // is defining them. 109 | case "hello": 110 | return "world" 111 | default: 112 | return nil 113 | } 114 | }, 115 | }) 116 | 117 | got := ty.CapsuleExtensionData("hello") 118 | want := interface{}("world") 119 | if got != want { 120 | t.Errorf("wrong result for 'hello'\ngot: %#v\nwant: %#v", got, want) 121 | } 122 | 123 | got = ty.CapsuleExtensionData("nonexistent") 124 | want = nil 125 | if got != want { 126 | t.Errorf("wrong result for 'nonexistent'\ngot: %#v\nwant: %#v", got, want) 127 | } 128 | 129 | ty2 := Capsule("without extension data", reflect.TypeOf(0)) 130 | got = ty2.CapsuleExtensionData("hello") 131 | want = nil 132 | if got != want { 133 | t.Errorf("wrong result for 'hello' without extension data\ngot: %#v\nwant: %#v", got, want) 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /cty/collection.go: -------------------------------------------------------------------------------- 1 | package cty 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | type collectionTypeImpl interface { 8 | ElementType() Type 9 | } 10 | 11 | // IsCollectionType returns true if the given type supports the operations 12 | // that are defined for all collection types. 13 | func (t Type) IsCollectionType() bool { 14 | _, ok := t.typeImpl.(collectionTypeImpl) 15 | return ok 16 | } 17 | 18 | // ElementType returns the element type of the receiver if it is a collection 19 | // type, or panics if it is not. Use IsCollectionType first to test whether 20 | // this method will succeed. 21 | func (t Type) ElementType() Type { 22 | if ct, ok := t.typeImpl.(collectionTypeImpl); ok { 23 | return ct.ElementType() 24 | } 25 | panic(errors.New("not a collection type")) 26 | } 27 | 28 | // ElementCallback is a callback type used for iterating over elements of 29 | // collections and attributes of objects. 30 | // 31 | // The types of key and value depend on what type is being iterated over. 32 | // Return true to stop iterating after the current element, or false to 33 | // continue iterating. 34 | type ElementCallback func(key Value, val Value) (stop bool) 35 | -------------------------------------------------------------------------------- /cty/convert/compare_types.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "github.com/zclconf/go-cty/cty" 5 | ) 6 | 7 | // compareTypes implements a preference order for unification. 8 | // 9 | // The result of this method is not useful for anything other than unification 10 | // preferences, since it assumes that the caller will verify that any suggested 11 | // conversion is actually possible and it is thus able to to make certain 12 | // optimistic assumptions. 13 | func compareTypes(a cty.Type, b cty.Type) int { 14 | 15 | // DynamicPseudoType always has lowest preference, because anything can 16 | // convert to it (it acts as a placeholder for "any type") and we want 17 | // to optimistically assume that any dynamics will converge on matching 18 | // their neighbors. 19 | if a == cty.DynamicPseudoType || b == cty.DynamicPseudoType { 20 | if a != cty.DynamicPseudoType { 21 | return -1 22 | } 23 | if b != cty.DynamicPseudoType { 24 | return 1 25 | } 26 | return 0 27 | } 28 | 29 | if a.IsPrimitiveType() && b.IsPrimitiveType() { 30 | // String is a supertype of all primitive types, because we can 31 | // represent all primitive values as specially-formatted strings. 32 | if a == cty.String || b == cty.String { 33 | if a != cty.String { 34 | return 1 35 | } 36 | if b != cty.String { 37 | return -1 38 | } 39 | return 0 40 | } 41 | } 42 | 43 | if a.IsListType() && b.IsListType() { 44 | return compareTypes(a.ElementType(), b.ElementType()) 45 | } 46 | if a.IsSetType() && b.IsSetType() { 47 | return compareTypes(a.ElementType(), b.ElementType()) 48 | } 49 | if a.IsMapType() && b.IsMapType() { 50 | return compareTypes(a.ElementType(), b.ElementType()) 51 | } 52 | 53 | // From this point on we may have swapped the two items in order to 54 | // simplify our cases. Therefore any non-zero return after this point 55 | // must be multiplied by "swap" to potentially invert the return value 56 | // if needed. 57 | swap := 1 58 | switch { 59 | case a.IsTupleType() && b.IsListType(): 60 | fallthrough 61 | case a.IsObjectType() && b.IsMapType(): 62 | fallthrough 63 | case a.IsSetType() && b.IsTupleType(): 64 | fallthrough 65 | case a.IsSetType() && b.IsListType(): 66 | a, b = b, a 67 | swap = -1 68 | } 69 | 70 | if b.IsSetType() && (a.IsTupleType() || a.IsListType()) { 71 | // We'll just optimistically assume that the element types are 72 | // unifyable/convertible, and let a second recursive pass 73 | // figure out how to make that so. 74 | return -1 * swap 75 | } 76 | 77 | if a.IsListType() && b.IsTupleType() { 78 | // We'll just optimistically assume that the tuple's element types 79 | // can be unified into something compatible with the list's element 80 | // type. 81 | return -1 * swap 82 | } 83 | 84 | if a.IsMapType() && b.IsObjectType() { 85 | // We'll just optimistically assume that the object's attribute types 86 | // can be unified into something compatible with the map's element 87 | // type. 88 | return -1 * swap 89 | } 90 | 91 | // For object and tuple types, comparing two types doesn't really tell 92 | // the whole story because it may be possible to construct a new type C 93 | // that is the supertype of both A and B by unifying each attribute/element 94 | // separately. That possibility is handled by Unify as a follow-up if 95 | // type sorting is insufficient to produce a valid result. 96 | // 97 | // Here we will take care of the simple possibilities where no new type 98 | // is needed. 99 | if a.IsObjectType() && b.IsObjectType() { 100 | atysA := a.AttributeTypes() 101 | atysB := b.AttributeTypes() 102 | 103 | if len(atysA) != len(atysB) { 104 | return 0 105 | } 106 | 107 | hasASuper := false 108 | hasBSuper := false 109 | for k := range atysA { 110 | if _, has := atysB[k]; !has { 111 | return 0 112 | } 113 | 114 | cmp := compareTypes(atysA[k], atysB[k]) 115 | if cmp < 0 { 116 | hasASuper = true 117 | } else if cmp > 0 { 118 | hasBSuper = true 119 | } 120 | } 121 | 122 | switch { 123 | case hasASuper && hasBSuper: 124 | return 0 125 | case hasASuper: 126 | return -1 * swap 127 | case hasBSuper: 128 | return 1 * swap 129 | default: 130 | return 0 131 | } 132 | } 133 | if a.IsTupleType() && b.IsTupleType() { 134 | etysA := a.TupleElementTypes() 135 | etysB := b.TupleElementTypes() 136 | 137 | if len(etysA) != len(etysB) { 138 | return 0 139 | } 140 | 141 | hasASuper := false 142 | hasBSuper := false 143 | for i := range etysA { 144 | cmp := compareTypes(etysA[i], etysB[i]) 145 | if cmp < 0 { 146 | hasASuper = true 147 | } else if cmp > 0 { 148 | hasBSuper = true 149 | } 150 | } 151 | 152 | switch { 153 | case hasASuper && hasBSuper: 154 | return 0 155 | case hasASuper: 156 | return -1 * swap 157 | case hasBSuper: 158 | return 1 * swap 159 | default: 160 | return 0 161 | } 162 | } 163 | 164 | return 0 165 | } 166 | -------------------------------------------------------------------------------- /cty/convert/conversion_capsule.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "github.com/zclconf/go-cty/cty" 5 | ) 6 | 7 | func conversionToCapsule(inTy, outTy cty.Type, fn func(inTy cty.Type) func(cty.Value, cty.Path) (interface{}, error)) conversion { 8 | rawConv := fn(inTy) 9 | if rawConv == nil { 10 | return nil 11 | } 12 | 13 | return func(in cty.Value, path cty.Path) (cty.Value, error) { 14 | rawV, err := rawConv(in, path) 15 | if err != nil { 16 | return cty.NilVal, err 17 | } 18 | return cty.CapsuleVal(outTy, rawV), nil 19 | } 20 | } 21 | 22 | func conversionFromCapsule(inTy, outTy cty.Type, fn func(outTy cty.Type) func(interface{}, cty.Path) (cty.Value, error)) conversion { 23 | rawConv := fn(outTy) 24 | if rawConv == nil { 25 | return nil 26 | } 27 | 28 | return func(in cty.Value, path cty.Path) (cty.Value, error) { 29 | return rawConv(in.EncapsulatedValue(), path) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /cty/convert/conversion_capsule_test.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/zclconf/go-cty/cty" 9 | ) 10 | 11 | func TestConvertCapsuleType(t *testing.T) { 12 | capTy := cty.CapsuleWithOps("test thingy", reflect.TypeOf(""), &cty.CapsuleOps{ 13 | GoString: func(rawV interface{}) string { 14 | vPtr := rawV.(*string) 15 | return fmt.Sprintf("capTy(%q)", *vPtr) 16 | }, 17 | TypeGoString: func(ty reflect.Type) string { 18 | return "capTy" 19 | }, 20 | RawEquals: func(a, b interface{}) bool { 21 | aPtr := a.(*string) 22 | bPtr := b.(*string) 23 | return *aPtr == *bPtr 24 | }, 25 | ConversionFrom: func(srcTy cty.Type) func(interface{}, cty.Path) (cty.Value, error) { 26 | if !srcTy.Equals(cty.String) { 27 | return nil 28 | } 29 | return func(rawV interface{}, path cty.Path) (cty.Value, error) { 30 | vPtr := rawV.(*string) 31 | return cty.StringVal(*vPtr), nil 32 | } 33 | }, 34 | ConversionTo: func(dstTy cty.Type) func(cty.Value, cty.Path) (interface{}, error) { 35 | if !dstTy.Equals(cty.String) { 36 | return nil 37 | } 38 | return func(from cty.Value, path cty.Path) (interface{}, error) { 39 | s := from.AsString() 40 | return &s, nil 41 | } 42 | }, 43 | }) 44 | 45 | capVal := func(s string) cty.Value { 46 | return cty.CapsuleVal(capTy, &s) 47 | } 48 | 49 | capIntTy := cty.CapsuleWithOps("int test thingy", reflect.TypeOf(0), &cty.CapsuleOps{ 50 | ConversionFrom: func(src cty.Type) func(interface{}, cty.Path) (cty.Value, error) { 51 | if src.Equals(capTy) { 52 | return func(v interface{}, p cty.Path) (cty.Value, error) { 53 | return capVal(fmt.Sprintf("%d", *(v.(*int)))), nil 54 | } 55 | } 56 | return nil 57 | }, 58 | }) 59 | capIntVal := func(i int) cty.Value { 60 | return cty.CapsuleVal(capIntTy, &i) 61 | } 62 | 63 | tests := []struct { 64 | From cty.Value 65 | To cty.Type 66 | Want cty.Value 67 | WantErr string 68 | }{ 69 | { 70 | From: capVal("hello"), 71 | To: cty.String, 72 | Want: cty.StringVal("hello"), 73 | }, 74 | { 75 | From: cty.StringVal("hello"), 76 | To: capTy, 77 | Want: capVal("hello"), 78 | }, 79 | { 80 | From: cty.True, 81 | To: capTy, 82 | WantErr: `test thingy required, but have bool`, 83 | }, 84 | { 85 | From: capVal("hello"), 86 | To: cty.Bool, 87 | WantErr: `bool required, but have test thingy`, 88 | }, 89 | { 90 | From: cty.UnknownVal(capTy), 91 | To: cty.String, 92 | Want: cty.UnknownVal(cty.String), 93 | }, 94 | { 95 | From: cty.NullVal(capTy), 96 | To: cty.String, 97 | Want: cty.NullVal(cty.String), 98 | }, 99 | { 100 | From: cty.UnknownVal(cty.Bool), 101 | To: capTy, 102 | WantErr: `test thingy required, but have bool`, 103 | }, 104 | { 105 | From: cty.NullVal(cty.Bool), 106 | To: capTy, 107 | WantErr: `test thingy required, but have bool`, 108 | }, 109 | { 110 | From: cty.UnknownVal(capTy), 111 | To: cty.Bool, 112 | WantErr: `bool required, but have test thingy`, 113 | }, 114 | { 115 | From: cty.NullVal(capTy), 116 | To: cty.Bool, 117 | WantErr: `bool required, but have test thingy`, 118 | }, 119 | { 120 | From: capIntVal(42), 121 | To: capTy, 122 | Want: capVal("42"), 123 | }, 124 | } 125 | 126 | for _, test := range tests { 127 | t.Run(fmt.Sprintf("%#v to %#v", test.From, test.To), func(t *testing.T) { 128 | got, err := Convert(test.From, test.To) 129 | 130 | if test.WantErr == "" { 131 | if err != nil { 132 | t.Fatalf("wrong error\nwant: \ngot: %s", err) 133 | } 134 | if !test.Want.RawEquals(got) { 135 | t.Errorf("wrong result\nwant: %#v\ngot: %#v", got, test.Want) 136 | } 137 | } else { 138 | if err == nil { 139 | t.Fatalf("wrong error\nwant: %s\ngot: ", test.WantErr) 140 | } 141 | if got, want := err.Error(), test.WantErr; got != want { 142 | t.Errorf("wrong error\nwant: %s\ngot: %s", got, want) 143 | } 144 | } 145 | }) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /cty/convert/conversion_object.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "github.com/zclconf/go-cty/cty" 5 | ) 6 | 7 | // conversionObjectToObject returns a conversion that will make the input 8 | // object type conform to the output object type, if possible. 9 | // 10 | // Conversion is possible only if the output type is a subset of the input 11 | // type, meaning that each attribute of the output type has a corresponding 12 | // attribute in the input type where a recursive conversion is available. 13 | // 14 | // If the "out" type has any optional attributes, those attributes may be 15 | // absent in the "in" type, in which case null values will be used in their 16 | // place in the result. 17 | // 18 | // Shallow object conversions work the same for both safe and unsafe modes, 19 | // but the safety flag is passed on to recursive conversions and may thus 20 | // limit the above definition of "subset". 21 | func conversionObjectToObject(in, out cty.Type, unsafe bool) conversion { 22 | inAtys := in.AttributeTypes() 23 | outAtys := out.AttributeTypes() 24 | outOptionals := out.OptionalAttributes() 25 | attrConvs := make(map[string]conversion) 26 | 27 | for name, outAty := range outAtys { 28 | inAty, exists := inAtys[name] 29 | if !exists { 30 | if _, optional := outOptionals[name]; optional { 31 | // If it's optional then we'll skip inserting an 32 | // attribute conversion and then deal with inserting 33 | // the default value in our overall conversion logic 34 | // later. 35 | continue 36 | } 37 | // No conversion is available, then. 38 | return nil 39 | } 40 | 41 | if inAty.Equals(outAty) { 42 | // No conversion needed, but we'll still record the attribute 43 | // in our map for later reference. 44 | attrConvs[name] = nil 45 | continue 46 | } 47 | 48 | attrConvs[name] = getConversion(inAty, outAty, unsafe) 49 | if attrConvs[name] == nil { 50 | // If a recursive conversion isn't available, then our top-level 51 | // configuration is impossible too. 52 | return nil 53 | } 54 | } 55 | 56 | // If we get here then a conversion is possible, using the attribute 57 | // conversions given in attrConvs. 58 | return func(val cty.Value, path cty.Path) (cty.Value, error) { 59 | attrVals := make(map[string]cty.Value, len(attrConvs)) 60 | path = append(path, nil) 61 | pathStep := &path[len(path)-1] 62 | 63 | for it := val.ElementIterator(); it.Next(); { 64 | nameVal, val := it.Element() 65 | var err error 66 | 67 | name := nameVal.AsString() 68 | *pathStep = cty.GetAttrStep{ 69 | Name: name, 70 | } 71 | 72 | conv, exists := attrConvs[name] 73 | if !exists { 74 | continue 75 | } 76 | if conv != nil { 77 | val, err = conv(val, path) 78 | if err != nil { 79 | return cty.NilVal, err 80 | } 81 | } 82 | 83 | if val.IsNull() { 84 | // Strip optional attributes out of the embedded type for null 85 | // values. 86 | val = cty.NullVal(val.Type().WithoutOptionalAttributesDeep()) 87 | } 88 | 89 | attrVals[name] = val 90 | } 91 | 92 | for name := range outOptionals { 93 | if _, exists := attrVals[name]; !exists { 94 | wantTy := outAtys[name] 95 | attrVals[name] = cty.NullVal(wantTy.WithoutOptionalAttributesDeep()) 96 | } 97 | } 98 | 99 | return cty.ObjectVal(attrVals), nil 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /cty/convert/conversion_primitive.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/zclconf/go-cty/cty" 7 | ) 8 | 9 | var stringTrue = cty.StringVal("true") 10 | var stringFalse = cty.StringVal("false") 11 | 12 | var primitiveConversionsSafe = map[cty.Type]map[cty.Type]conversion{ 13 | cty.Number: { 14 | cty.String: func(val cty.Value, path cty.Path) (cty.Value, error) { 15 | f := val.AsBigFloat() 16 | return cty.StringVal(f.Text('f', -1)), nil 17 | }, 18 | }, 19 | cty.Bool: { 20 | cty.String: func(val cty.Value, path cty.Path) (cty.Value, error) { 21 | if val.True() { 22 | return stringTrue, nil 23 | } else { 24 | return stringFalse, nil 25 | } 26 | }, 27 | }, 28 | } 29 | 30 | var primitiveConversionsUnsafe = map[cty.Type]map[cty.Type]conversion{ 31 | cty.String: { 32 | cty.Number: func(val cty.Value, path cty.Path) (cty.Value, error) { 33 | v, err := cty.ParseNumberVal(val.AsString()) 34 | if err != nil { 35 | return cty.NilVal, path.NewErrorf("a number is required") 36 | } 37 | return v, nil 38 | }, 39 | cty.Bool: func(val cty.Value, path cty.Path) (cty.Value, error) { 40 | switch val.AsString() { 41 | case "true", "1": 42 | return cty.True, nil 43 | case "false", "0": 44 | return cty.False, nil 45 | default: 46 | switch strings.ToLower(val.AsString()) { 47 | case "true": 48 | return cty.NilVal, path.NewErrorf("a bool is required; to convert from string, use lowercase \"true\"") 49 | case "false": 50 | return cty.NilVal, path.NewErrorf("a bool is required; to convert from string, use lowercase \"false\"") 51 | default: 52 | return cty.NilVal, path.NewErrorf("a bool is required") 53 | } 54 | } 55 | }, 56 | }, 57 | } 58 | -------------------------------------------------------------------------------- /cty/convert/conversion_tuple.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "github.com/zclconf/go-cty/cty" 5 | ) 6 | 7 | // conversionTupleToTuple returns a conversion that will make the input 8 | // tuple type conform to the output tuple type, if possible. 9 | // 10 | // Conversion is possible only if the two tuple types have the same number 11 | // of elements and the corresponding elements by index can be converted. 12 | // 13 | // Shallow tuple conversions work the same for both safe and unsafe modes, 14 | // but the safety flag is passed on to recursive conversions and may thus 15 | // limit which element type conversions are possible. 16 | func conversionTupleToTuple(in, out cty.Type, unsafe bool) conversion { 17 | inEtys := in.TupleElementTypes() 18 | outEtys := out.TupleElementTypes() 19 | 20 | if len(inEtys) != len(outEtys) { 21 | return nil // no conversion is possible 22 | } 23 | 24 | elemConvs := make([]conversion, len(inEtys)) 25 | 26 | for i, outEty := range outEtys { 27 | inEty := inEtys[i] 28 | 29 | if inEty.Equals(outEty) { 30 | // No conversion needed, so we can leave this one nil. 31 | continue 32 | } 33 | 34 | elemConvs[i] = getConversion(inEty, outEty, unsafe) 35 | if elemConvs[i] == nil { 36 | // If a recursive conversion isn't available, then our top-level 37 | // configuration is impossible too. 38 | return nil 39 | } 40 | } 41 | 42 | // If we get here then a conversion is possible, using the element 43 | // conversions given in elemConvs. 44 | return func(val cty.Value, path cty.Path) (cty.Value, error) { 45 | elemVals := make([]cty.Value, len(elemConvs)) 46 | path = append(path, nil) 47 | pathStep := &path[len(path)-1] 48 | 49 | i := 0 50 | for it := val.ElementIterator(); it.Next(); i++ { 51 | _, val := it.Element() 52 | var err error 53 | 54 | *pathStep = cty.IndexStep{ 55 | Key: cty.NumberIntVal(int64(i)), 56 | } 57 | 58 | conv := elemConvs[i] 59 | if conv != nil { 60 | val, err = conv(val, path) 61 | if err != nil { 62 | return cty.NilVal, err 63 | } 64 | } 65 | 66 | elemVals[i] = val 67 | } 68 | 69 | return cty.TupleVal(elemVals), nil 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /cty/convert/doc.go: -------------------------------------------------------------------------------- 1 | // Package convert contains some routines for converting between cty types. 2 | // The intent of providing this package is to encourage applications using 3 | // cty to have consistent type conversion behavior for maximal interoperability 4 | // when Values pass from one application to another. 5 | // 6 | // The conversions are categorized into two categories. "Safe" conversions are 7 | // ones that are guaranteed to succeed if given a non-null value of the 8 | // appropriate source type. "Unsafe" conversions, on the other hand, are valid 9 | // for only a subset of input values, and thus may fail with an error when 10 | // called for values outside of that valid subset. 11 | // 12 | // The functions whose names end in Unsafe support all of the conversions that 13 | // are supported by the corresponding functions whose names do not have that 14 | // suffix, and then additional unsafe conversions as well. 15 | package convert 16 | -------------------------------------------------------------------------------- /cty/convert/mismatch_msg_test.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/zclconf/go-cty/cty" 8 | ) 9 | 10 | func TestMismatchMessage(t *testing.T) { 11 | tests := []struct { 12 | GotType, WantType cty.Type 13 | WantMsg string 14 | }{ 15 | { 16 | cty.Bool, 17 | cty.Number, 18 | `number required, but have bool`, 19 | }, 20 | { 21 | cty.EmptyObject, 22 | cty.Object(map[string]cty.Type{ 23 | "foo": cty.String, 24 | }), 25 | `attribute "foo" is required`, 26 | }, 27 | { 28 | cty.EmptyObject, 29 | cty.Object(map[string]cty.Type{ 30 | "foo": cty.String, 31 | "bar": cty.String, 32 | }), 33 | `attributes "bar" and "foo" are required`, 34 | }, 35 | { 36 | cty.EmptyObject, 37 | cty.Object(map[string]cty.Type{ 38 | "foo": cty.String, 39 | "bar": cty.String, 40 | "baz": cty.String, 41 | }), 42 | `attributes "bar", "baz", and "foo" are required`, 43 | }, 44 | { 45 | cty.EmptyObject, 46 | cty.List(cty.Object(map[string]cty.Type{ 47 | "foo": cty.String, 48 | "bar": cty.String, 49 | "baz": cty.String, 50 | })), 51 | `list of object required`, 52 | }, 53 | { 54 | cty.List(cty.String), 55 | cty.List(cty.Object(map[string]cty.Type{ 56 | "foo": cty.String, 57 | })), 58 | `incorrect list element type: object required, but have string`, 59 | }, 60 | { 61 | cty.List(cty.EmptyObject), 62 | cty.List(cty.Object(map[string]cty.Type{ 63 | "foo": cty.String, 64 | })), 65 | `incorrect list element type: attribute "foo" is required`, 66 | }, 67 | { 68 | cty.Tuple([]cty.Type{cty.EmptyObject}), 69 | cty.List(cty.Object(map[string]cty.Type{ 70 | "foo": cty.String, 71 | })), 72 | `element 0: attribute "foo" is required`, 73 | }, 74 | { 75 | cty.List(cty.EmptyObject), 76 | cty.Set(cty.Object(map[string]cty.Type{ 77 | "foo": cty.String, 78 | })), 79 | `incorrect set element type: attribute "foo" is required`, 80 | }, 81 | { 82 | cty.Tuple([]cty.Type{cty.EmptyObject}), 83 | cty.Set(cty.Object(map[string]cty.Type{ 84 | "foo": cty.String, 85 | })), 86 | `element 0: attribute "foo" is required`, 87 | }, 88 | { 89 | cty.Map(cty.EmptyObject), 90 | cty.Map(cty.Object(map[string]cty.Type{ 91 | "foo": cty.String, 92 | })), 93 | `incorrect map element type: attribute "foo" is required`, 94 | }, 95 | { 96 | cty.Object(map[string]cty.Type{"boop": cty.EmptyObject}), 97 | cty.Map(cty.Object(map[string]cty.Type{ 98 | "foo": cty.String, 99 | })), 100 | `element "boop": attribute "foo" is required`, 101 | }, 102 | { 103 | cty.Tuple([]cty.Type{cty.EmptyObject, cty.EmptyTuple}), 104 | cty.List(cty.DynamicPseudoType), 105 | `all list elements must have the same type`, 106 | }, 107 | { 108 | cty.Object(map[string]cty.Type{ 109 | "foo": cty.Bool, 110 | "bar": cty.String, 111 | "baz": cty.Object(map[string]cty.Type{ 112 | "boop": cty.Number, 113 | }), 114 | }), 115 | cty.Object(map[string]cty.Type{ 116 | "foo": cty.Bool, 117 | "bar": cty.String, 118 | "baz": cty.Object(map[string]cty.Type{ 119 | "boop": cty.Number, 120 | "beep": cty.Bool, 121 | }), 122 | }), 123 | `attribute "baz": attribute "beep" is required`, 124 | }, 125 | } 126 | 127 | for _, test := range tests { 128 | t.Run(fmt.Sprintf("%#v but want %#v", test.GotType, test.WantType), func(t *testing.T) { 129 | got := MismatchMessage(test.GotType, test.WantType) 130 | if got != test.WantMsg { 131 | t.Errorf("wrong message\ngot type: %#v\nwant type: %#v\ngot message: %s\nwant message: %s", test.GotType, test.WantType, got, test.WantMsg) 132 | } 133 | }) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /cty/convert/public.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/zclconf/go-cty/cty" 7 | ) 8 | 9 | // This file contains the public interface of this package, which is intended 10 | // to be a small, convenient interface designed for easy integration into 11 | // a hypothetical language type checker and interpreter. 12 | 13 | // Conversion is a named function type representing a conversion from a 14 | // value of one type to a value of another type. 15 | // 16 | // The source type for a conversion is always the source type given to 17 | // the function that returned the Conversion, but there is no way to recover 18 | // that from a Conversion value itself. If a Conversion is given a value 19 | // that is not of its expected type (with the exception of DynamicPseudoType, 20 | // which is always supported) then the function may panic or produce undefined 21 | // results. 22 | type Conversion func(in cty.Value) (out cty.Value, err error) 23 | 24 | // GetConversion returns a Conversion between the given in and out Types if 25 | // a safe one is available, or returns nil otherwise. 26 | func GetConversion(in cty.Type, out cty.Type) Conversion { 27 | return retConversion(getConversion(in, out, false)) 28 | } 29 | 30 | // GetConversionUnsafe returns a Conversion between the given in and out Types 31 | // if either a safe or unsafe one is available, or returns nil otherwise. 32 | func GetConversionUnsafe(in cty.Type, out cty.Type) Conversion { 33 | return retConversion(getConversion(in, out, true)) 34 | } 35 | 36 | // Convert returns the result of converting the given value to the given type 37 | // if an safe or unsafe conversion is available, or returns an error if such a 38 | // conversion is impossible. 39 | // 40 | // This is a convenience wrapper around calling GetConversionUnsafe and then 41 | // immediately passing the given value to the resulting function. 42 | func Convert(in cty.Value, want cty.Type) (cty.Value, error) { 43 | if in.Type().Equals(want.WithoutOptionalAttributesDeep()) { 44 | return in, nil 45 | } 46 | 47 | conv := GetConversionUnsafe(in.Type(), want) 48 | if conv == nil { 49 | return cty.NilVal, errors.New(MismatchMessage(in.Type(), want)) 50 | } 51 | return conv(in) 52 | } 53 | 54 | // Unify attempts to find the most general type that can be converted from 55 | // all of the given types. If this is possible, that type is returned along 56 | // with a slice of necessary conversions for some of the given types. 57 | // 58 | // If no common supertype can be found, this function returns cty.NilType and 59 | // a nil slice. 60 | // 61 | // If a common supertype *can* be found, the returned slice will always be 62 | // non-nil and will contain a non-nil conversion for each given type that 63 | // needs to be converted, with indices corresponding to the input slice. 64 | // Any given type that does *not* need conversion (because it is already of 65 | // the appropriate type) will have a nil Conversion. 66 | // 67 | // cty.DynamicPseudoType is, as usual, a special case. If the given type list 68 | // contains a mixture of dynamic and non-dynamic types, the dynamic types are 69 | // disregarded for type selection and a conversion is returned for them that 70 | // will attempt a late conversion of the given value to the target type, 71 | // failing with a conversion error if the eventual concrete type is not 72 | // compatible. If *all* given types are DynamicPseudoType, or in the 73 | // degenerate case of an empty slice of types, the returned type is itself 74 | // cty.DynamicPseudoType and no conversions are attempted. 75 | func Unify(types []cty.Type) (cty.Type, []Conversion) { 76 | return unify(types, false) 77 | } 78 | 79 | // UnifyUnsafe is the same as Unify except that it may return unsafe 80 | // conversions in situations where a safe conversion isn't also available. 81 | func UnifyUnsafe(types []cty.Type) (cty.Type, []Conversion) { 82 | return unify(types, true) 83 | } 84 | -------------------------------------------------------------------------------- /cty/convert/sort_types.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "github.com/zclconf/go-cty/cty" 5 | ) 6 | 7 | // sortTypes produces an ordering of the given types that serves as a 8 | // preference order for the result of unification of the given types. 9 | // The return value is a slice of indices into the given slice, and will 10 | // thus always be the same length as the given slice. 11 | // 12 | // The goal is that the most general of the given types will appear first 13 | // in the ordering. If there are uncomparable pairs of types in the list 14 | // then they will appear in an undefined order, and the unification pass 15 | // will presumably then fail. 16 | func sortTypes(tys []cty.Type) []int { 17 | l := len(tys) 18 | 19 | // First we build a graph whose edges represent "more general than", 20 | // which we will then do a topological sort of. 21 | edges := make([][]int, l) 22 | for i := 0; i < (l - 1); i++ { 23 | for j := i + 1; j < l; j++ { 24 | cmp := compareTypes(tys[i], tys[j]) 25 | switch { 26 | case cmp < 0: 27 | edges[i] = append(edges[i], j) 28 | case cmp > 0: 29 | edges[j] = append(edges[j], i) 30 | } 31 | } 32 | } 33 | 34 | // Compute the in-degree of each node 35 | inDegree := make([]int, l) 36 | for _, outs := range edges { 37 | for _, j := range outs { 38 | inDegree[j]++ 39 | } 40 | } 41 | 42 | // The array backing our result will double as our queue for visiting 43 | // the nodes, with the queue slice moving along this array until it 44 | // is empty and positioned at the end of the array. Thus our visiting 45 | // order is also our result order. 46 | result := make([]int, l) 47 | queue := result[0:0] 48 | 49 | // Initialize the queue with any item of in-degree 0, preserving 50 | // their relative order. 51 | for i, n := range inDegree { 52 | if n == 0 { 53 | queue = append(queue, i) 54 | } 55 | } 56 | 57 | for len(queue) != 0 { 58 | i := queue[0] 59 | queue = queue[1:] 60 | for _, j := range edges[i] { 61 | inDegree[j]-- 62 | if inDegree[j] == 0 { 63 | queue = append(queue, j) 64 | } 65 | } 66 | } 67 | 68 | return result 69 | } 70 | -------------------------------------------------------------------------------- /cty/convert/sort_types_test.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/zclconf/go-cty/cty" 8 | ) 9 | 10 | func TestSortTypes(t *testing.T) { 11 | tests := []struct { 12 | Input []cty.Type 13 | Want []cty.Type 14 | }{ 15 | { 16 | []cty.Type{}, 17 | []cty.Type{}, 18 | }, 19 | { 20 | []cty.Type{cty.String, cty.Number}, 21 | []cty.Type{cty.String, cty.Number}, 22 | }, 23 | { 24 | []cty.Type{cty.Number, cty.String}, 25 | []cty.Type{cty.String, cty.Number}, 26 | }, 27 | { 28 | []cty.Type{cty.String, cty.Bool}, 29 | []cty.Type{cty.String, cty.Bool}, 30 | }, 31 | { 32 | []cty.Type{cty.Bool, cty.String}, 33 | []cty.Type{cty.String, cty.Bool}, 34 | }, 35 | { 36 | []cty.Type{cty.Bool, cty.String, cty.Number}, 37 | []cty.Type{cty.String, cty.Bool, cty.Number}, 38 | }, 39 | { 40 | []cty.Type{cty.Number, cty.String, cty.Bool}, 41 | []cty.Type{cty.String, cty.Number, cty.Bool}, 42 | }, 43 | { 44 | []cty.Type{cty.String, cty.String}, 45 | []cty.Type{cty.String, cty.String}, 46 | }, 47 | { 48 | []cty.Type{cty.Number, cty.String, cty.Number}, 49 | []cty.Type{cty.String, cty.Number, cty.Number}, 50 | }, 51 | { 52 | []cty.Type{cty.String, cty.List(cty.String)}, 53 | []cty.Type{cty.String, cty.List(cty.String)}, 54 | }, 55 | { 56 | []cty.Type{cty.List(cty.String), cty.String}, 57 | []cty.Type{cty.List(cty.String), cty.String}, 58 | }, 59 | { 60 | // This result is somewhat arbitrary, but the important thing 61 | // is that it is consistent. 62 | []cty.Type{cty.Bool, cty.List(cty.String), cty.String}, 63 | []cty.Type{cty.List(cty.String), cty.String, cty.Bool}, 64 | }, 65 | { 66 | []cty.Type{cty.String, cty.DynamicPseudoType}, 67 | []cty.Type{cty.String, cty.DynamicPseudoType}, 68 | }, 69 | { 70 | []cty.Type{cty.DynamicPseudoType, cty.String}, 71 | []cty.Type{cty.String, cty.DynamicPseudoType}, 72 | }, 73 | } 74 | 75 | for _, test := range tests { 76 | t.Run(fmt.Sprintf("%#v", test.Input), func(t *testing.T) { 77 | idxs := sortTypes(test.Input) 78 | 79 | if len(idxs) != len(test.Input) { 80 | t.Fatalf("wrong number of indexes %q; want %q", len(idxs), len(test.Input)) 81 | } 82 | 83 | got := make([]cty.Type, len(idxs)) 84 | 85 | for i, idx := range idxs { 86 | got[i] = test.Input[idx] 87 | } 88 | 89 | for i := range test.Want { 90 | if !got[i].Equals(test.Want[i]) { 91 | t.Errorf( 92 | "wrong order\ninput: %#v\ngot: %#v\nwant: %#v", 93 | test.Input, 94 | got, test.Want, 95 | ) 96 | break 97 | } 98 | } 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /cty/ctystrings/doc.go: -------------------------------------------------------------------------------- 1 | // Package ctystrings is a collection of string manipulation utilities which 2 | // intend to help application developers implement string-manipulation 3 | // functionality in a way that respects the cty model of strings, even when 4 | // they are working in the realm of Go strings. 5 | // 6 | // cty strings are, internally, NFC-normalized as defined in Unicode Standard 7 | // Annex #15 and encoded as UTF-8. 8 | // 9 | // When working with [cty.Value] of string type cty manages this 10 | // automatically as an implementation detail, but when applications call 11 | // [Value.AsString] they will receive a value that has been subjected to that 12 | // normalization, and so may need to take that normalization into account when 13 | // manipulating the resulting string or comparing it with other Go strings 14 | // that did not originate in a [cty.Value]. 15 | // 16 | // Although the core representation of [cty.String] only considers whole 17 | // strings, it's also conventional in other locations such as the standard 18 | // library functions to consider strings as being sequences of grapheme 19 | // clusters as defined by Unicode Standard Annex #29, which adds further 20 | // rules about combining multiple consecutive codepoints together into a 21 | // single user-percieved character. Functions that work with substrings should 22 | // always use grapheme clusters as their smallest unit of splitting strings, 23 | // and never break strings in the middle of a grapheme cluster. The functions 24 | // in this package respect that convention unless otherwise stated in their 25 | // documentation. 26 | package ctystrings 27 | -------------------------------------------------------------------------------- /cty/ctystrings/normalize.go: -------------------------------------------------------------------------------- 1 | package ctystrings 2 | 3 | import ( 4 | "golang.org/x/text/unicode/norm" 5 | ) 6 | 7 | // Normalize applies NFC normalization to the given string, returning the 8 | // transformed string. 9 | // 10 | // This function achieves the same effect as wrapping a string in a value 11 | // using [cty.StringVal] and then unwrapping it again using [Value.AsString]. 12 | func Normalize(str string) string { 13 | return norm.NFC.String(str) 14 | } 15 | -------------------------------------------------------------------------------- /cty/doc.go: -------------------------------------------------------------------------------- 1 | // Package cty (pronounced see-tie) provides some infrastructure for a type 2 | // system that might be useful for applications that need to represent 3 | // configuration values provided by the user whose types are not known 4 | // at compile time, particularly if the calling application also allows 5 | // such values to be used in expressions. 6 | // 7 | // The type system consists of primitive types Number, String and Bool, as 8 | // well as List and Map collection types and Object types that can have 9 | // arbitrarily-typed sets of attributes. 10 | // 11 | // A set of operations is defined on these types, which is accessible via 12 | // the wrapper struct Value, which annotates the raw, internal representation 13 | // of a value with its corresponding type. 14 | // 15 | // This package is oriented towards being a building block for configuration 16 | // languages used to bootstrap an application. It is not optimized for use 17 | // in tight loops where CPU time or memory pressure are a concern. 18 | package cty 19 | -------------------------------------------------------------------------------- /cty/element_iterator.go: -------------------------------------------------------------------------------- 1 | package cty 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/zclconf/go-cty/cty/set" 7 | ) 8 | 9 | // ElementIterator is the interface type returned by Value.ElementIterator to 10 | // allow the caller to iterate over elements of a collection-typed value. 11 | // 12 | // Its usage pattern is as follows: 13 | // 14 | // it := val.ElementIterator() 15 | // for it.Next() { 16 | // key, val := it.Element() 17 | // // ... 18 | // } 19 | type ElementIterator interface { 20 | Next() bool 21 | Element() (key Value, value Value) 22 | } 23 | 24 | func canElementIterator(val Value) bool { 25 | switch { 26 | case val.IsMarked(): 27 | return false 28 | case val.ty.IsListType(): 29 | return true 30 | case val.ty.IsMapType(): 31 | return true 32 | case val.ty.IsSetType(): 33 | return true 34 | case val.ty.IsTupleType(): 35 | return true 36 | case val.ty.IsObjectType(): 37 | return true 38 | default: 39 | return false 40 | } 41 | } 42 | 43 | func elementIterator(val Value) ElementIterator { 44 | val.assertUnmarked() 45 | switch { 46 | case val.ty.IsListType(): 47 | return &listElementIterator{ 48 | ety: val.ty.ElementType(), 49 | vals: val.v.([]interface{}), 50 | idx: -1, 51 | } 52 | case val.ty.IsMapType(): 53 | // We iterate the keys in a predictable lexicographical order so 54 | // that results will always be stable given the same input map. 55 | rawMap := val.v.(map[string]interface{}) 56 | keys := make([]string, 0, len(rawMap)) 57 | for key := range rawMap { 58 | keys = append(keys, key) 59 | } 60 | sort.Strings(keys) 61 | 62 | return &mapElementIterator{ 63 | ety: val.ty.ElementType(), 64 | vals: rawMap, 65 | keys: keys, 66 | idx: -1, 67 | } 68 | case val.ty.IsSetType(): 69 | rawSet := val.v.(set.Set[interface{}]) 70 | return &setElementIterator{ 71 | ety: val.ty.ElementType(), 72 | setIt: rawSet.Iterator(), 73 | } 74 | case val.ty.IsTupleType(): 75 | return &tupleElementIterator{ 76 | etys: val.ty.TupleElementTypes(), 77 | vals: val.v.([]interface{}), 78 | idx: -1, 79 | } 80 | case val.ty.IsObjectType(): 81 | // We iterate the keys in a predictable lexicographical order so 82 | // that results will always be stable given the same object type. 83 | atys := val.ty.AttributeTypes() 84 | keys := make([]string, 0, len(atys)) 85 | for key := range atys { 86 | keys = append(keys, key) 87 | } 88 | sort.Strings(keys) 89 | 90 | return &objectElementIterator{ 91 | atys: atys, 92 | vals: val.v.(map[string]interface{}), 93 | attrNames: keys, 94 | idx: -1, 95 | } 96 | default: 97 | panic("attempt to iterate on non-collection, non-tuple type") 98 | } 99 | } 100 | 101 | type listElementIterator struct { 102 | ety Type 103 | vals []interface{} 104 | idx int 105 | } 106 | 107 | func (it *listElementIterator) Element() (Value, Value) { 108 | i := it.idx 109 | return NumberIntVal(int64(i)), Value{ 110 | ty: it.ety, 111 | v: it.vals[i], 112 | } 113 | } 114 | 115 | func (it *listElementIterator) Next() bool { 116 | it.idx++ 117 | return it.idx < len(it.vals) 118 | } 119 | 120 | type mapElementIterator struct { 121 | ety Type 122 | vals map[string]interface{} 123 | keys []string 124 | idx int 125 | } 126 | 127 | func (it *mapElementIterator) Element() (Value, Value) { 128 | key := it.keys[it.idx] 129 | return StringVal(key), Value{ 130 | ty: it.ety, 131 | v: it.vals[key], 132 | } 133 | } 134 | 135 | func (it *mapElementIterator) Next() bool { 136 | it.idx++ 137 | return it.idx < len(it.keys) 138 | } 139 | 140 | type setElementIterator struct { 141 | ety Type 142 | setIt *set.Iterator[interface{}] 143 | } 144 | 145 | func (it *setElementIterator) Element() (Value, Value) { 146 | val := Value{ 147 | ty: it.ety, 148 | v: it.setIt.Value(), 149 | } 150 | return val, val 151 | } 152 | 153 | func (it *setElementIterator) Next() bool { 154 | return it.setIt.Next() 155 | } 156 | 157 | type tupleElementIterator struct { 158 | etys []Type 159 | vals []interface{} 160 | idx int 161 | } 162 | 163 | func (it *tupleElementIterator) Element() (Value, Value) { 164 | i := it.idx 165 | return NumberIntVal(int64(i)), Value{ 166 | ty: it.etys[i], 167 | v: it.vals[i], 168 | } 169 | } 170 | 171 | func (it *tupleElementIterator) Next() bool { 172 | it.idx++ 173 | return it.idx < len(it.vals) 174 | } 175 | 176 | type objectElementIterator struct { 177 | atys map[string]Type 178 | vals map[string]interface{} 179 | attrNames []string 180 | idx int 181 | } 182 | 183 | func (it *objectElementIterator) Element() (Value, Value) { 184 | key := it.attrNames[it.idx] 185 | return StringVal(key), Value{ 186 | ty: it.atys[key], 187 | v: it.vals[key], 188 | } 189 | } 190 | 191 | func (it *objectElementIterator) Next() bool { 192 | it.idx++ 193 | return it.idx < len(it.attrNames) 194 | } 195 | -------------------------------------------------------------------------------- /cty/error.go: -------------------------------------------------------------------------------- 1 | package cty 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // PathError is a specialization of error that represents where in a 8 | // potentially-deep data structure an error occured, using a Path. 9 | type PathError struct { 10 | error 11 | Path Path 12 | } 13 | 14 | func errorf(path Path, f string, args ...interface{}) error { 15 | // We need to copy the Path because often our caller builds it by 16 | // continually mutating the same underlying buffer. 17 | sPath := make(Path, len(path)) 18 | copy(sPath, path) 19 | return PathError{ 20 | error: fmt.Errorf(f, args...), 21 | Path: sPath, 22 | } 23 | } 24 | 25 | // NewErrorf creates a new PathError for the current path by passing the 26 | // given format and arguments to fmt.Errorf and then wrapping the result 27 | // similarly to NewError. 28 | func (p Path) NewErrorf(f string, args ...interface{}) error { 29 | return errorf(p, f, args...) 30 | } 31 | 32 | // NewError creates a new PathError for the current path, wrapping the given 33 | // error. 34 | func (p Path) NewError(err error) error { 35 | // if we're being asked to wrap an existing PathError then our new 36 | // PathError will be the concatenation of the two paths, ensuring 37 | // that we still get a single flat PathError that's thus easier for 38 | // callers to deal with. 39 | perr, wrappingPath := err.(PathError) 40 | pathLen := len(p) 41 | if wrappingPath { 42 | pathLen = pathLen + len(perr.Path) 43 | } 44 | 45 | sPath := make(Path, pathLen) 46 | copy(sPath, p) 47 | if wrappingPath { 48 | copy(sPath[len(p):], perr.Path) 49 | } 50 | 51 | return PathError{ 52 | error: err, 53 | Path: sPath, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /cty/function/argument.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import ( 4 | "github.com/zclconf/go-cty/cty" 5 | ) 6 | 7 | // Parameter represents a parameter to a function. 8 | type Parameter struct { 9 | // Name is an optional name for the argument. This package ignores this 10 | // value, but callers may use it for documentation, etc. 11 | Name string 12 | 13 | // Description is an optional description for the argument. 14 | Description string 15 | 16 | // A type that any argument for this parameter must conform to. 17 | // cty.DynamicPseudoType can be used, either at top-level or nested 18 | // in a parameterized type, to indicate that any type should be 19 | // permitted, to allow the definition of type-generic functions. 20 | Type cty.Type 21 | 22 | // If AllowNull is set then null values may be passed into this 23 | // argument's slot in both the type-check function and the implementation 24 | // function. If not set, such values are rejected by the built-in 25 | // checking rules. 26 | AllowNull bool 27 | 28 | // If AllowUnknown is set then unknown values may be passed into this 29 | // argument's slot in the implementation function. If not set, any 30 | // unknown values will cause the function to immediately return 31 | // an unkonwn value without calling the implementation function, thus 32 | // freeing the function implementer from dealing with this case. 33 | AllowUnknown bool 34 | 35 | // If AllowDynamicType is set then DynamicVal may be passed into this 36 | // argument's slot in the implementation function. If not set, any 37 | // dynamic values will cause the function to immediately return 38 | // DynamicVal value without calling the implementation function, thus 39 | // freeing the function implementer from dealing with this case. 40 | // 41 | // Note that DynamicVal is also unknown, so in order to receive dynamic 42 | // *values* it is also necessary to set AllowUnknown. 43 | // 44 | // However, it is valid to set AllowDynamicType without AllowUnknown, in 45 | // which case a dynamic value may be passed to the type checking function 46 | // but will not make it to the *implementation* function. Instead, an 47 | // unknown value of the type returned by the type-check function will be 48 | // returned. This is suggested for functions that have a static return 49 | // type since it allows the return value to be typed even if the input 50 | // values are not, thus improving the type-check accuracy of derived 51 | // values. 52 | AllowDynamicType bool 53 | 54 | // If AllowMarked is set then marked values may be passed into this 55 | // argument's slot in the implementation function. If not set, any 56 | // marked value will be unmarked before calling and then the markings 57 | // from that value will be applied automatically to the function result, 58 | // ensuring that the marks get propagated in a simplistic way even if 59 | // a function is unable to handle them. 60 | // 61 | // For any argument whose parameter has AllowMarked set, it's the 62 | // function implementation's responsibility to Unmark the given value 63 | // and propagate the marks appropriatedly to the result in order to 64 | // avoid losing the marks. Application-specific functions might use 65 | // special rules to selectively propagate particular marks. 66 | // 67 | // The automatic unmarking of values applies only to the main 68 | // implementation function. In an application that uses marked values, 69 | // the Type implementation for a function must always be prepared to accept 70 | // marked values, which is easy to achieve by consulting only the type 71 | // and ignoring the value itself. 72 | AllowMarked bool 73 | } 74 | -------------------------------------------------------------------------------- /cty/function/doc.go: -------------------------------------------------------------------------------- 1 | // Package function builds on the functionality of cty by modeling functions 2 | // that operate on cty Values. 3 | // 4 | // Functions are, at their core, Go anonymous functions. However, this package 5 | // wraps around them utility functions for parameter type checking, etc. 6 | package function 7 | -------------------------------------------------------------------------------- /cty/function/error.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import ( 4 | "fmt" 5 | "runtime/debug" 6 | ) 7 | 8 | // ArgError represents an error with one of the arguments in a call. The 9 | // attribute Index represents the zero-based index of the argument in question. 10 | // 11 | // Its error *may* be a cty.PathError, in which case the error actually 12 | // pertains to a nested value within the data structure passed as the argument. 13 | type ArgError struct { 14 | error 15 | Index int 16 | } 17 | 18 | func NewArgErrorf(i int, f string, args ...interface{}) error { 19 | return ArgError{ 20 | error: fmt.Errorf(f, args...), 21 | Index: i, 22 | } 23 | } 24 | 25 | func NewArgError(i int, err error) error { 26 | return ArgError{ 27 | error: err, 28 | Index: i, 29 | } 30 | } 31 | 32 | // PanicError indicates that a panic occurred while executing either a 33 | // function's type or implementation function. This is captured and wrapped 34 | // into a normal error so that callers (expected to be language runtimes) 35 | // are freed from having to deal with panics in buggy functions. 36 | type PanicError struct { 37 | Value interface{} 38 | Stack []byte 39 | } 40 | 41 | func errorForPanic(val interface{}) error { 42 | return PanicError{ 43 | Value: val, 44 | Stack: debug.Stack(), 45 | } 46 | } 47 | 48 | func (e PanicError) Error() string { 49 | return fmt.Sprintf("panic in function implementation: %s\n%s", e.Value, e.Stack) 50 | } 51 | -------------------------------------------------------------------------------- /cty/function/stdlib/bool.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "github.com/zclconf/go-cty/cty" 5 | "github.com/zclconf/go-cty/cty/function" 6 | ) 7 | 8 | var NotFunc = function.New(&function.Spec{ 9 | Description: `Applies the logical NOT operation to the given boolean value.`, 10 | Params: []function.Parameter{ 11 | { 12 | Name: "val", 13 | Type: cty.Bool, 14 | AllowDynamicType: true, 15 | AllowMarked: true, 16 | }, 17 | }, 18 | Type: function.StaticReturnType(cty.Bool), 19 | RefineResult: refineNonNull, 20 | Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 21 | return args[0].Not(), nil 22 | }, 23 | }) 24 | 25 | var AndFunc = function.New(&function.Spec{ 26 | Description: `Applies the logical AND operation to the given boolean values.`, 27 | Params: []function.Parameter{ 28 | { 29 | Name: "a", 30 | Type: cty.Bool, 31 | AllowDynamicType: true, 32 | AllowMarked: true, 33 | }, 34 | { 35 | Name: "b", 36 | Type: cty.Bool, 37 | AllowDynamicType: true, 38 | AllowMarked: true, 39 | }, 40 | }, 41 | Type: function.StaticReturnType(cty.Bool), 42 | RefineResult: refineNonNull, 43 | Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 44 | return args[0].And(args[1]), nil 45 | }, 46 | }) 47 | 48 | var OrFunc = function.New(&function.Spec{ 49 | Description: `Applies the logical OR operation to the given boolean values.`, 50 | Params: []function.Parameter{ 51 | { 52 | Name: "a", 53 | Type: cty.Bool, 54 | AllowDynamicType: true, 55 | AllowMarked: true, 56 | }, 57 | { 58 | Name: "b", 59 | Type: cty.Bool, 60 | AllowDynamicType: true, 61 | AllowMarked: true, 62 | }, 63 | }, 64 | Type: function.StaticReturnType(cty.Bool), 65 | RefineResult: refineNonNull, 66 | Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 67 | return args[0].Or(args[1]), nil 68 | }, 69 | }) 70 | 71 | // Not returns the logical complement of the given boolean value. 72 | func Not(num cty.Value) (cty.Value, error) { 73 | return NotFunc.Call([]cty.Value{num}) 74 | } 75 | 76 | // And returns true if and only if both of the given boolean values are true. 77 | func And(a, b cty.Value) (cty.Value, error) { 78 | return AndFunc.Call([]cty.Value{a, b}) 79 | } 80 | 81 | // Or returns true if either of the given boolean values are true. 82 | func Or(a, b cty.Value) (cty.Value, error) { 83 | return OrFunc.Call([]cty.Value{a, b}) 84 | } 85 | -------------------------------------------------------------------------------- /cty/function/stdlib/bool_test.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/zclconf/go-cty/cty" 8 | ) 9 | 10 | func TestNot(t *testing.T) { 11 | tests := []struct { 12 | Input cty.Value 13 | Want cty.Value 14 | }{ 15 | { 16 | cty.True, 17 | cty.False, 18 | }, 19 | { 20 | cty.False, 21 | cty.True, 22 | }, 23 | { 24 | cty.UnknownVal(cty.Bool), 25 | cty.UnknownVal(cty.Bool).RefineNotNull(), 26 | }, 27 | { 28 | cty.DynamicVal, 29 | cty.UnknownVal(cty.Bool).RefineNotNull(), 30 | }, 31 | { 32 | cty.True.Mark(1), 33 | cty.False.Mark(1), 34 | }, 35 | } 36 | 37 | for _, test := range tests { 38 | t.Run(fmt.Sprintf("Not(%#v)", test.Input), func(t *testing.T) { 39 | got, err := Not(test.Input) 40 | 41 | if err != nil { 42 | t.Fatalf("unexpected error: %s", err) 43 | } 44 | 45 | if !got.RawEquals(test.Want) { 46 | t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) 47 | } 48 | }) 49 | } 50 | } 51 | 52 | func TestAnd(t *testing.T) { 53 | tests := []struct { 54 | A cty.Value 55 | B cty.Value 56 | Want cty.Value 57 | }{ 58 | { 59 | cty.False, 60 | cty.False, 61 | cty.False, 62 | }, 63 | { 64 | cty.False, 65 | cty.True, 66 | cty.False, 67 | }, 68 | { 69 | cty.True, 70 | cty.False, 71 | cty.False, 72 | }, 73 | { 74 | cty.True, 75 | cty.True, 76 | cty.True, 77 | }, 78 | { 79 | cty.True, 80 | cty.UnknownVal(cty.Bool), 81 | cty.UnknownVal(cty.Bool).RefineNotNull(), 82 | }, 83 | { 84 | cty.UnknownVal(cty.Bool), 85 | cty.UnknownVal(cty.Bool), 86 | cty.UnknownVal(cty.Bool).RefineNotNull(), 87 | }, 88 | { 89 | cty.True, 90 | cty.DynamicVal, 91 | cty.UnknownVal(cty.Bool).RefineNotNull(), 92 | }, 93 | { 94 | cty.DynamicVal, 95 | cty.DynamicVal, 96 | cty.UnknownVal(cty.Bool).RefineNotNull(), 97 | }, 98 | } 99 | 100 | for _, test := range tests { 101 | t.Run(fmt.Sprintf("And(%#v,%#v)", test.A, test.B), func(t *testing.T) { 102 | got, err := And(test.A, test.B) 103 | 104 | if err != nil { 105 | t.Fatalf("unexpected error: %s", err) 106 | } 107 | 108 | if !got.RawEquals(test.Want) { 109 | t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) 110 | } 111 | }) 112 | } 113 | } 114 | 115 | func TestOr(t *testing.T) { 116 | tests := []struct { 117 | A cty.Value 118 | B cty.Value 119 | Want cty.Value 120 | }{ 121 | { 122 | cty.False, 123 | cty.False, 124 | cty.False, 125 | }, 126 | { 127 | cty.False, 128 | cty.True, 129 | cty.True, 130 | }, 131 | { 132 | cty.True, 133 | cty.False, 134 | cty.True, 135 | }, 136 | { 137 | cty.True, 138 | cty.True, 139 | cty.True, 140 | }, 141 | { 142 | cty.True, 143 | cty.UnknownVal(cty.Bool), 144 | cty.UnknownVal(cty.Bool).RefineNotNull(), 145 | }, 146 | { 147 | cty.UnknownVal(cty.Bool), 148 | cty.UnknownVal(cty.Bool), 149 | cty.UnknownVal(cty.Bool).RefineNotNull(), 150 | }, 151 | { 152 | cty.True, 153 | cty.DynamicVal, 154 | cty.UnknownVal(cty.Bool).RefineNotNull(), 155 | }, 156 | { 157 | cty.DynamicVal, 158 | cty.DynamicVal, 159 | cty.UnknownVal(cty.Bool).RefineNotNull(), 160 | }, 161 | } 162 | 163 | for _, test := range tests { 164 | t.Run(fmt.Sprintf("Or(%#v,%#v)", test.A, test.B), func(t *testing.T) { 165 | got, err := Or(test.A, test.B) 166 | 167 | if err != nil { 168 | t.Fatalf("unexpected error: %s", err) 169 | } 170 | 171 | if !got.RawEquals(test.Want) { 172 | t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) 173 | } 174 | }) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /cty/function/stdlib/bytes.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/zclconf/go-cty/cty" 8 | "github.com/zclconf/go-cty/cty/function" 9 | "github.com/zclconf/go-cty/cty/gocty" 10 | ) 11 | 12 | // Bytes is a capsule type that can be used with the binary functions to 13 | // support applications that need to support raw buffers in addition to 14 | // UTF-8 strings. 15 | var Bytes = cty.Capsule("bytes", reflect.TypeOf([]byte(nil))) 16 | 17 | // BytesVal creates a new Bytes value from the given buffer, which must be 18 | // non-nil or this function will panic. 19 | // 20 | // Once a byte slice has been wrapped in a Bytes capsule, its underlying array 21 | // must be considered immutable. 22 | func BytesVal(buf []byte) cty.Value { 23 | if buf == nil { 24 | panic("can't make Bytes value from nil slice") 25 | } 26 | 27 | return cty.CapsuleVal(Bytes, &buf) 28 | } 29 | 30 | // BytesLen is a Function that returns the length of the buffer encapsulated 31 | // in a Bytes value. 32 | var BytesLenFunc = function.New(&function.Spec{ 33 | Description: `Returns the total number of bytes in the given buffer.`, 34 | Params: []function.Parameter{ 35 | { 36 | Name: "buf", 37 | Type: Bytes, 38 | AllowDynamicType: true, 39 | }, 40 | }, 41 | Type: function.StaticReturnType(cty.Number), 42 | RefineResult: refineNonNull, 43 | Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 44 | bufPtr := args[0].EncapsulatedValue().(*[]byte) 45 | return cty.NumberIntVal(int64(len(*bufPtr))), nil 46 | }, 47 | }) 48 | 49 | // BytesSlice is a Function that returns a slice of the given Bytes value. 50 | var BytesSliceFunc = function.New(&function.Spec{ 51 | Description: `Extracts a subslice from the given buffer.`, 52 | Params: []function.Parameter{ 53 | { 54 | Name: "buf", 55 | Type: Bytes, 56 | AllowDynamicType: true, 57 | }, 58 | { 59 | Name: "offset", 60 | Type: cty.Number, 61 | AllowDynamicType: true, 62 | }, 63 | { 64 | Name: "length", 65 | Type: cty.Number, 66 | AllowDynamicType: true, 67 | }, 68 | }, 69 | Type: function.StaticReturnType(Bytes), 70 | RefineResult: refineNonNull, 71 | Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 72 | bufPtr := args[0].EncapsulatedValue().(*[]byte) 73 | 74 | var offset, length int 75 | 76 | var err error 77 | err = gocty.FromCtyValue(args[1], &offset) 78 | if err != nil { 79 | return cty.NilVal, err 80 | } 81 | err = gocty.FromCtyValue(args[2], &length) 82 | if err != nil { 83 | return cty.NilVal, err 84 | } 85 | 86 | if offset < 0 || length < 0 { 87 | return cty.NilVal, fmt.Errorf("offset and length must be non-negative") 88 | } 89 | 90 | if offset > len(*bufPtr) { 91 | return cty.NilVal, fmt.Errorf( 92 | "offset %d is greater than total buffer length %d", 93 | offset, len(*bufPtr), 94 | ) 95 | } 96 | 97 | end := offset + length 98 | 99 | if end > len(*bufPtr) { 100 | return cty.NilVal, fmt.Errorf( 101 | "offset %d + length %d is greater than total buffer length %d", 102 | offset, length, len(*bufPtr), 103 | ) 104 | } 105 | 106 | return BytesVal((*bufPtr)[offset:end]), nil 107 | }, 108 | }) 109 | 110 | func BytesLen(buf cty.Value) (cty.Value, error) { 111 | return BytesLenFunc.Call([]cty.Value{buf}) 112 | } 113 | 114 | func BytesSlice(buf cty.Value, offset cty.Value, length cty.Value) (cty.Value, error) { 115 | return BytesSliceFunc.Call([]cty.Value{buf, offset, length}) 116 | } 117 | -------------------------------------------------------------------------------- /cty/function/stdlib/bytes_test.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/zclconf/go-cty/cty" 8 | ) 9 | 10 | func TestBytesLen(t *testing.T) { 11 | tests := []struct { 12 | Input cty.Value 13 | Want cty.Value 14 | }{ 15 | { 16 | BytesVal([]byte{}), 17 | cty.NumberIntVal(0), 18 | }, 19 | { 20 | BytesVal([]byte{'a'}), 21 | cty.NumberIntVal(1), 22 | }, 23 | { 24 | BytesVal([]byte{'a', 'b', 'c'}), 25 | cty.NumberIntVal(3), 26 | }, 27 | } 28 | 29 | for _, test := range tests { 30 | t.Run(test.Input.GoString(), func(t *testing.T) { 31 | got, err := BytesLen(test.Input) 32 | 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | if !got.RawEquals(test.Want) { 38 | t.Errorf( 39 | "wrong result\ninput: %#v\ngot: %#v\nwant: %#v", 40 | test.Input, got, test.Want, 41 | ) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | func TestBytesSlice(t *testing.T) { 48 | tests := []struct { 49 | Input cty.Value 50 | Offset cty.Value 51 | Length cty.Value 52 | Want cty.Value 53 | }{ 54 | { 55 | BytesVal([]byte{}), 56 | cty.NumberIntVal(0), 57 | cty.NumberIntVal(0), 58 | BytesVal([]byte{}), 59 | }, 60 | { 61 | BytesVal([]byte{'a'}), 62 | cty.NumberIntVal(0), 63 | cty.NumberIntVal(1), 64 | BytesVal([]byte{'a'}), 65 | }, 66 | { 67 | BytesVal([]byte{'a', 'b', 'c'}), 68 | cty.NumberIntVal(0), 69 | cty.NumberIntVal(2), 70 | BytesVal([]byte{'a', 'b'}), 71 | }, 72 | { 73 | BytesVal([]byte{'a', 'b', 'c'}), 74 | cty.NumberIntVal(1), 75 | cty.NumberIntVal(2), 76 | BytesVal([]byte{'b', 'c'}), 77 | }, 78 | { 79 | BytesVal([]byte{'a', 'b', 'c'}), 80 | cty.NumberIntVal(0), 81 | cty.NumberIntVal(3), 82 | BytesVal([]byte{'a', 'b', 'c'}), 83 | }, 84 | } 85 | 86 | for _, test := range tests { 87 | t.Run(test.Input.GoString(), func(t *testing.T) { 88 | got, err := BytesSlice(test.Input, test.Offset, test.Length) 89 | 90 | if err != nil { 91 | t.Fatal(err) 92 | } 93 | 94 | gotBytes := *(got.EncapsulatedValue().(*[]byte)) 95 | wantBytes := *(test.Want.EncapsulatedValue().(*[]byte)) 96 | 97 | if !reflect.DeepEqual(gotBytes, wantBytes) { 98 | t.Errorf( 99 | "wrong result\ninput: %#v, %#v, %#v\ngot: %#v\nwant: %#v", 100 | test.Input, test.Offset, test.Length, got, test.Want, 101 | ) 102 | } 103 | }) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /cty/function/stdlib/conversion_test.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/zclconf/go-cty/cty" 8 | ) 9 | 10 | func TestTo(t *testing.T) { 11 | tests := []struct { 12 | Value cty.Value 13 | TargetTy cty.Type 14 | Want cty.Value 15 | Err string 16 | }{ 17 | { 18 | cty.StringVal("a"), 19 | cty.String, 20 | cty.StringVal("a"), 21 | ``, 22 | }, 23 | { 24 | cty.UnknownVal(cty.String), 25 | cty.String, 26 | cty.UnknownVal(cty.String), 27 | ``, 28 | }, 29 | { 30 | cty.NullVal(cty.String), 31 | cty.String, 32 | cty.NullVal(cty.String), 33 | ``, 34 | }, 35 | { 36 | cty.True, 37 | cty.String, 38 | cty.StringVal("true"), 39 | ``, 40 | }, 41 | { 42 | cty.StringVal("a"), 43 | cty.Bool, 44 | cty.DynamicVal, 45 | `cannot convert "a" to bool; only the strings "true" or "false" are allowed`, 46 | }, 47 | { 48 | cty.StringVal("a"), 49 | cty.Number, 50 | cty.DynamicVal, 51 | `cannot convert "a" to number; given string must be a decimal representation of a number`, 52 | }, 53 | { 54 | cty.NullVal(cty.String), 55 | cty.Number, 56 | cty.NullVal(cty.Number), 57 | ``, 58 | }, 59 | { 60 | cty.NullVal(cty.DynamicPseudoType), 61 | cty.Number, 62 | cty.NullVal(cty.Number), 63 | ``, 64 | }, 65 | { 66 | cty.UnknownVal(cty.Bool), 67 | cty.String, 68 | cty.UnknownVal(cty.String), 69 | ``, 70 | }, 71 | { 72 | cty.UnknownVal(cty.String), 73 | cty.Bool, 74 | cty.UnknownVal(cty.Bool), // conversion is optimistic 75 | ``, 76 | }, 77 | { 78 | cty.TupleVal([]cty.Value{cty.StringVal("hello"), cty.True}), 79 | cty.List(cty.String), 80 | cty.ListVal([]cty.Value{cty.StringVal("hello"), cty.StringVal("true")}), 81 | ``, 82 | }, 83 | { 84 | cty.TupleVal([]cty.Value{cty.StringVal("hello"), cty.True}), 85 | cty.Set(cty.String), 86 | cty.SetVal([]cty.Value{cty.StringVal("hello"), cty.StringVal("true")}), 87 | ``, 88 | }, 89 | { 90 | cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("hello"), "bar": cty.True}), 91 | cty.Map(cty.String), 92 | cty.MapVal(map[string]cty.Value{"foo": cty.StringVal("hello"), "bar": cty.StringVal("true")}), 93 | ``, 94 | }, 95 | { 96 | cty.EmptyTupleVal, 97 | cty.String, 98 | cty.DynamicVal, 99 | `cannot convert tuple to string`, 100 | }, 101 | { 102 | cty.UnknownVal(cty.EmptyTuple), 103 | cty.String, 104 | cty.DynamicVal, 105 | `cannot convert tuple to string`, 106 | }, 107 | { 108 | cty.EmptyObjectVal, 109 | cty.Object(map[string]cty.Type{"foo": cty.String}), 110 | cty.DynamicVal, 111 | `incompatible object type for conversion: attribute "foo" is required`, 112 | }, 113 | } 114 | 115 | for _, test := range tests { 116 | t.Run(fmt.Sprintf("to %s(%#v)", test.TargetTy.FriendlyNameForConstraint(), test.Value), func(t *testing.T) { 117 | f := MakeToFunc(test.TargetTy) 118 | got, err := f.Call([]cty.Value{test.Value}) 119 | 120 | if test.Err != "" { 121 | if err == nil { 122 | t.Fatal("succeeded; want error") 123 | } 124 | if got, want := err.Error(), test.Err; got != want { 125 | t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want) 126 | } 127 | return 128 | } else if err != nil { 129 | t.Fatalf("unexpected error: %s", err) 130 | } 131 | 132 | if !got.RawEquals(test.Want) { 133 | t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) 134 | } 135 | }) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /cty/function/stdlib/csv.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "encoding/csv" 5 | "fmt" 6 | "io" 7 | "strings" 8 | 9 | "github.com/zclconf/go-cty/cty" 10 | "github.com/zclconf/go-cty/cty/function" 11 | ) 12 | 13 | var CSVDecodeFunc = function.New(&function.Spec{ 14 | Description: `Parses the given string as Comma Separated Values (as defined by RFC 4180) and returns a map of objects representing the table of data, using the first row as a header row to define the object attributes.`, 15 | Params: []function.Parameter{ 16 | { 17 | Name: "str", 18 | Type: cty.String, 19 | }, 20 | }, 21 | Type: func(args []cty.Value) (cty.Type, error) { 22 | str := args[0] 23 | if !str.IsKnown() { 24 | return cty.DynamicPseudoType, nil 25 | } 26 | 27 | r := strings.NewReader(str.AsString()) 28 | cr := csv.NewReader(r) 29 | headers, err := cr.Read() 30 | if err == io.EOF { 31 | return cty.DynamicPseudoType, fmt.Errorf("missing header line") 32 | } 33 | if err != nil { 34 | return cty.DynamicPseudoType, csvError(err) 35 | } 36 | 37 | atys := make(map[string]cty.Type, len(headers)) 38 | for _, name := range headers { 39 | if _, exists := atys[name]; exists { 40 | return cty.DynamicPseudoType, fmt.Errorf("duplicate column name %q", name) 41 | } 42 | atys[name] = cty.String 43 | } 44 | return cty.List(cty.Object(atys)), nil 45 | }, 46 | RefineResult: refineNonNull, 47 | Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 48 | ety := retType.ElementType() 49 | atys := ety.AttributeTypes() 50 | str := args[0] 51 | r := strings.NewReader(str.AsString()) 52 | cr := csv.NewReader(r) 53 | cr.FieldsPerRecord = len(atys) 54 | 55 | // Read the header row first, since that'll tell us which indices 56 | // map to which attribute names. 57 | headers, err := cr.Read() 58 | if err != nil { 59 | return cty.DynamicVal, err 60 | } 61 | 62 | var rows []cty.Value 63 | for { 64 | cols, err := cr.Read() 65 | if err == io.EOF { 66 | break 67 | } 68 | if err != nil { 69 | return cty.DynamicVal, csvError(err) 70 | } 71 | 72 | vals := make(map[string]cty.Value, len(cols)) 73 | for i, str := range cols { 74 | name := headers[i] 75 | vals[name] = cty.StringVal(str) 76 | } 77 | rows = append(rows, cty.ObjectVal(vals)) 78 | } 79 | 80 | if len(rows) == 0 { 81 | return cty.ListValEmpty(ety), nil 82 | } 83 | return cty.ListVal(rows), nil 84 | }, 85 | }) 86 | 87 | // CSVDecode parses the given CSV (RFC 4180) string and, if it is valid, 88 | // returns a list of objects representing the rows. 89 | // 90 | // The result is always a list of some object type. The first row of the 91 | // input is used to determine the object attributes, and subsequent rows 92 | // determine the values of those attributes. 93 | func CSVDecode(str cty.Value) (cty.Value, error) { 94 | return CSVDecodeFunc.Call([]cty.Value{str}) 95 | } 96 | 97 | func csvError(err error) error { 98 | switch err := err.(type) { 99 | case *csv.ParseError: 100 | return fmt.Errorf("CSV parse error on line %d: %w", err.Line, err.Err) 101 | default: 102 | return err 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /cty/function/stdlib/csv_test.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/zclconf/go-cty/cty" 8 | ) 9 | 10 | func TestCSVDecode(t *testing.T) { 11 | tests := []struct { 12 | Input cty.Value 13 | Want cty.Value 14 | WantErr string 15 | }{ 16 | { 17 | cty.StringVal(csvTest), 18 | cty.ListVal([]cty.Value{ 19 | cty.ObjectVal(map[string]cty.Value{ 20 | "name": cty.StringVal("foo"), 21 | "size": cty.StringVal("100"), 22 | "type": cty.StringVal("tiny"), 23 | }), 24 | cty.ObjectVal(map[string]cty.Value{ 25 | "name": cty.StringVal("bar"), 26 | "size": cty.StringVal(""), 27 | "type": cty.StringVal("huge"), 28 | }), 29 | cty.ObjectVal(map[string]cty.Value{ 30 | "name": cty.StringVal("baz"), 31 | "size": cty.StringVal("50"), 32 | "type": cty.StringVal("weedy"), 33 | }), 34 | }), 35 | ``, 36 | }, 37 | { 38 | cty.StringVal(`"just","header","line"`), 39 | cty.ListValEmpty(cty.Object(map[string]cty.Type{ 40 | "just": cty.String, 41 | "header": cty.String, 42 | "line": cty.String, 43 | })), 44 | ``, 45 | }, 46 | { 47 | cty.StringVal(``), 48 | cty.DynamicVal, 49 | `missing header line`, 50 | }, 51 | { 52 | cty.StringVal(`not csv at all`), 53 | cty.ListValEmpty(cty.Object(map[string]cty.Type{ 54 | "not csv at all": cty.String, 55 | })), 56 | ``, 57 | }, 58 | { 59 | cty.StringVal(`invalid"thing"`), 60 | cty.DynamicVal, 61 | `CSV parse error on line 1: bare " in non-quoted-field`, 62 | }, 63 | { 64 | cty.UnknownVal(cty.String), 65 | cty.DynamicVal, // need to know the value to determine the type 66 | ``, 67 | }, 68 | { 69 | cty.DynamicVal, 70 | cty.DynamicVal, 71 | ``, 72 | }, 73 | { 74 | cty.True, 75 | cty.DynamicVal, 76 | `string required, but received bool`, 77 | }, 78 | { 79 | cty.NullVal(cty.String), 80 | cty.DynamicVal, 81 | `argument must not be null`, 82 | }, 83 | } 84 | 85 | for _, test := range tests { 86 | t.Run(fmt.Sprintf("CSVDecode(%#v)", test.Input), func(t *testing.T) { 87 | got, err := CSVDecode(test.Input) 88 | var errStr string 89 | if err != nil { 90 | errStr = err.Error() 91 | } 92 | if errStr != test.WantErr { 93 | t.Fatalf("wrong error\ngot: %s\nwant: %s", errStr, test.WantErr) 94 | } 95 | if err != nil { 96 | return 97 | } 98 | 99 | if !got.RawEquals(test.Want) { 100 | t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) 101 | } 102 | }) 103 | } 104 | } 105 | 106 | const csvTest = `"name","size","type" 107 | "foo","100","tiny" 108 | "bar","","huge" 109 | "baz","50","weedy" 110 | ` 111 | -------------------------------------------------------------------------------- /cty/function/stdlib/doc.go: -------------------------------------------------------------------------------- 1 | // Package stdlib is a collection of cty functions that are expected to be 2 | // generally useful, and are thus factored out into this shared library in 3 | // the hope that cty-using applications will have consistent behavior when 4 | // using these functions. 5 | // 6 | // See the parent package "function" for more information on the purpose 7 | // and usage of cty functions. 8 | // 9 | // This package contains both Go functions, which provide convenient access 10 | // to call the functions from Go code, and the Function objects themselves. 11 | // The latter follow the naming scheme of appending "Func" to the end of 12 | // the function name. 13 | package stdlib 14 | -------------------------------------------------------------------------------- /cty/function/stdlib/format_fsm.rl: -------------------------------------------------------------------------------- 1 | // This file is generated from format_fsm.rl. DO NOT EDIT. 2 | %%{ 3 | # (except you are actually in scan_tokens.rl here, so edit away!) 4 | machine formatfsm; 5 | }%% 6 | 7 | package stdlib 8 | 9 | import ( 10 | "bytes" 11 | "fmt" 12 | "unicode/utf8" 13 | 14 | "github.com/zclconf/go-cty/cty" 15 | "github.com/zclconf/go-cty/cty/function" 16 | ) 17 | 18 | %%{ 19 | write data; 20 | }%% 21 | 22 | func formatFSM(format string, a []cty.Value) (string, error) { 23 | var buf bytes.Buffer 24 | data := format 25 | nextArg := 1 // arg numbers are 1-based 26 | var verb formatVerb 27 | highestArgIdx := 0 // zero means "none", since arg numbers are 1-based 28 | 29 | %%{ 30 | 31 | action begin { 32 | verb = formatVerb{ 33 | ArgNum: nextArg, 34 | Prec: -1, 35 | Width: -1, 36 | } 37 | ts = p 38 | } 39 | 40 | action emit { 41 | buf.WriteByte(fc); 42 | } 43 | 44 | action finish_ok { 45 | } 46 | 47 | action finish_err { 48 | return buf.String(), fmt.Errorf("invalid format string starting at offset %d", p) 49 | } 50 | 51 | action err_char { 52 | // We'll try to slurp a whole UTF-8 sequence here, to give the user 53 | // better feedback. 54 | r, _ := utf8.DecodeRuneInString(data[p:]) 55 | return buf.String(), fmt.Errorf("unrecognized format character %q at offset %d", r, p) 56 | } 57 | 58 | action flag_sharp { 59 | verb.Sharp = true 60 | } 61 | action flag_zero { 62 | verb.Zero = true 63 | } 64 | action flag_minus { 65 | verb.Minus = true 66 | } 67 | action flag_plus { 68 | verb.Plus = true 69 | } 70 | action flag_space { 71 | verb.Space = true 72 | } 73 | 74 | action argidx_reset { 75 | verb.ArgNum = 0 76 | } 77 | action argidx_num { 78 | verb.ArgNum = (10 * verb.ArgNum) + (int(fc) - '0') 79 | } 80 | 81 | action has_width { 82 | verb.HasWidth = true 83 | } 84 | action width_reset { 85 | verb.Width = 0 86 | } 87 | action width_num { 88 | verb.Width = (10 * verb.Width) + (int(fc) - '0') 89 | } 90 | 91 | action has_prec { 92 | verb.HasPrec = true 93 | } 94 | action prec_reset { 95 | verb.Prec = 0 96 | } 97 | action prec_num { 98 | verb.Prec = (10 * verb.Prec) + (int(fc) - '0') 99 | } 100 | 101 | action mode { 102 | verb.Mode = rune(fc) 103 | te = p+1 104 | verb.Raw = data[ts:te] 105 | verb.Offset = ts 106 | 107 | if verb.ArgNum > highestArgIdx { 108 | highestArgIdx = verb.ArgNum 109 | } 110 | 111 | err := formatAppend(&verb, &buf, a) 112 | if err != nil { 113 | return buf.String(), err 114 | } 115 | nextArg = verb.ArgNum + 1 116 | } 117 | 118 | # a number that isn't zero and doesn't have a leading zero 119 | num = [1-9] [0-9]*; 120 | 121 | flags = ( 122 | '0' @flag_zero | 123 | '#' @flag_sharp | 124 | '-' @flag_minus | 125 | '+' @flag_plus | 126 | ' ' @flag_space 127 | )*; 128 | 129 | argidx = (( 130 | '[' (num $argidx_num) ']' 131 | ) >argidx_reset)?; 132 | 133 | width = ( 134 | ( num $width_num ) >width_reset %has_width 135 | )?; 136 | 137 | precision = ( 138 | ('.' ( digit* $prec_num )) >prec_reset %has_prec 139 | )?; 140 | 141 | # We accept any letter here, but will be more picky in formatAppend 142 | mode = ('a'..'z' | 'A'..'Z') @mode; 143 | 144 | fmt_verb = ( 145 | '%' @begin 146 | flags 147 | width 148 | precision 149 | argidx 150 | mode 151 | ); 152 | 153 | main := ( 154 | [^%] @emit | 155 | '%%' @emit | 156 | fmt_verb 157 | )* @/finish_err %/finish_ok $!err_char; 158 | 159 | }%% 160 | 161 | // Ragel state 162 | p := 0 // "Pointer" into data 163 | pe := len(data) // End-of-data "pointer" 164 | cs := 0 // current state (will be initialized by ragel-generated code) 165 | ts := 0 166 | te := 0 167 | eof := pe 168 | 169 | // Keep Go compiler happy even if generated code doesn't use these 170 | _ = ts 171 | _ = te 172 | _ = eof 173 | 174 | %%{ 175 | write init; 176 | write exec; 177 | }%% 178 | 179 | // If we fall out here without being in a final state then we've 180 | // encountered something that the scanner can't match, which should 181 | // be impossible (the scanner matches all bytes _somehow_) but we'll 182 | // flag it anyway rather than just losing data from the end. 183 | if cs < formatfsm_first_final { 184 | return buf.String(), fmt.Errorf("extraneous characters beginning at offset %d", p) 185 | } 186 | 187 | if highestArgIdx < len(a) { 188 | // Extraneous args are an error, to more easily detect mistakes 189 | firstBad := highestArgIdx+1 190 | if highestArgIdx == 0 { 191 | // Custom error message for this case 192 | return buf.String(), function.NewArgErrorf(firstBad, "too many arguments; no verbs in format string") 193 | } 194 | return buf.String(), function.NewArgErrorf(firstBad, "too many arguments; only %d used by format string", highestArgIdx) 195 | } 196 | 197 | return buf.String(), nil 198 | } 199 | -------------------------------------------------------------------------------- /cty/function/stdlib/general.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/zclconf/go-cty/cty" 7 | "github.com/zclconf/go-cty/cty/convert" 8 | "github.com/zclconf/go-cty/cty/function" 9 | ) 10 | 11 | var EqualFunc = function.New(&function.Spec{ 12 | Description: `Returns true if the two given values are equal, or false otherwise.`, 13 | Params: []function.Parameter{ 14 | { 15 | Name: "a", 16 | Type: cty.DynamicPseudoType, 17 | AllowUnknown: true, 18 | AllowDynamicType: true, 19 | AllowNull: true, 20 | }, 21 | { 22 | Name: "b", 23 | Type: cty.DynamicPseudoType, 24 | AllowUnknown: true, 25 | AllowDynamicType: true, 26 | AllowNull: true, 27 | }, 28 | }, 29 | Type: function.StaticReturnType(cty.Bool), 30 | RefineResult: refineNonNull, 31 | Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { 32 | return args[0].Equals(args[1]), nil 33 | }, 34 | }) 35 | 36 | var NotEqualFunc = function.New(&function.Spec{ 37 | Description: `Returns false if the two given values are equal, or true otherwise.`, 38 | Params: []function.Parameter{ 39 | { 40 | Name: "a", 41 | Type: cty.DynamicPseudoType, 42 | AllowUnknown: true, 43 | AllowDynamicType: true, 44 | AllowNull: true, 45 | }, 46 | { 47 | Name: "b", 48 | Type: cty.DynamicPseudoType, 49 | AllowUnknown: true, 50 | AllowDynamicType: true, 51 | AllowNull: true, 52 | }, 53 | }, 54 | Type: function.StaticReturnType(cty.Bool), 55 | RefineResult: refineNonNull, 56 | Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { 57 | return args[0].Equals(args[1]).Not(), nil 58 | }, 59 | }) 60 | 61 | var CoalesceFunc = function.New(&function.Spec{ 62 | Description: `Returns the first of the given arguments that isn't null, or raises an error if there are no non-null arguments.`, 63 | Params: []function.Parameter{}, 64 | VarParam: &function.Parameter{ 65 | Name: "vals", 66 | Type: cty.DynamicPseudoType, 67 | AllowUnknown: true, 68 | AllowDynamicType: true, 69 | AllowNull: true, 70 | }, 71 | Type: func(args []cty.Value) (ret cty.Type, err error) { 72 | argTypes := make([]cty.Type, len(args)) 73 | for i, val := range args { 74 | argTypes[i] = val.Type() 75 | } 76 | retType, _ := convert.UnifyUnsafe(argTypes) 77 | if retType == cty.NilType { 78 | return cty.NilType, fmt.Errorf("all arguments must have the same type") 79 | } 80 | return retType, nil 81 | }, 82 | RefineResult: refineNonNull, 83 | Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { 84 | for _, argVal := range args { 85 | if !argVal.IsKnown() { 86 | return cty.UnknownVal(retType), nil 87 | } 88 | if argVal.IsNull() { 89 | continue 90 | } 91 | 92 | return convert.Convert(argVal, retType) 93 | } 94 | return cty.NilVal, fmt.Errorf("no non-null arguments") 95 | }, 96 | }) 97 | 98 | func refineNonNull(b *cty.RefinementBuilder) *cty.RefinementBuilder { 99 | return b.NotNull() 100 | } 101 | 102 | // Equal determines whether the two given values are equal, returning a 103 | // bool value. 104 | func Equal(a cty.Value, b cty.Value) (cty.Value, error) { 105 | return EqualFunc.Call([]cty.Value{a, b}) 106 | } 107 | 108 | // NotEqual is the opposite of Equal. 109 | func NotEqual(a cty.Value, b cty.Value) (cty.Value, error) { 110 | return NotEqualFunc.Call([]cty.Value{a, b}) 111 | } 112 | 113 | // Coalesce returns the first of the given arguments that is not null. If 114 | // all arguments are null, an error is produced. 115 | func Coalesce(vals ...cty.Value) (cty.Value, error) { 116 | return CoalesceFunc.Call(vals) 117 | } 118 | -------------------------------------------------------------------------------- /cty/function/stdlib/general_test.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/zclconf/go-cty/cty" 8 | ) 9 | 10 | func TestEqual(t *testing.T) { 11 | tests := []struct { 12 | A cty.Value 13 | B cty.Value 14 | Want cty.Value 15 | }{ 16 | { 17 | cty.NumberIntVal(1), 18 | cty.NumberIntVal(2), 19 | cty.False, 20 | }, 21 | { 22 | cty.NumberIntVal(2), 23 | cty.NumberIntVal(2), 24 | cty.True, 25 | }, 26 | { 27 | cty.NullVal(cty.Number), 28 | cty.NullVal(cty.Number), 29 | cty.True, 30 | }, 31 | { 32 | cty.NumberIntVal(2), 33 | cty.NullVal(cty.Number), 34 | cty.False, 35 | }, 36 | { 37 | cty.NumberIntVal(1), 38 | cty.UnknownVal(cty.Number), 39 | cty.UnknownVal(cty.Bool).RefineNotNull(), 40 | }, 41 | { 42 | cty.UnknownVal(cty.Number), 43 | cty.UnknownVal(cty.Number), 44 | cty.UnknownVal(cty.Bool).RefineNotNull(), 45 | }, 46 | { 47 | cty.NumberIntVal(1), 48 | cty.DynamicVal, 49 | cty.UnknownVal(cty.Bool).RefineNotNull(), 50 | }, 51 | { 52 | cty.DynamicVal, 53 | cty.DynamicVal, 54 | cty.UnknownVal(cty.Bool).RefineNotNull(), 55 | }, 56 | } 57 | 58 | for _, test := range tests { 59 | t.Run(fmt.Sprintf("Equal(%#v,%#v)", test.A, test.B), func(t *testing.T) { 60 | got, err := Equal(test.A, test.B) 61 | 62 | if err != nil { 63 | t.Fatalf("unexpected error: %s", err) 64 | } 65 | 66 | if !got.RawEquals(test.Want) { 67 | t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) 68 | } 69 | }) 70 | } 71 | } 72 | 73 | func TestCoalesce(t *testing.T) { 74 | tests := []struct { 75 | Values []cty.Value 76 | Want cty.Value 77 | }{ 78 | { 79 | []cty.Value{cty.True}, 80 | cty.True, 81 | }, 82 | { 83 | []cty.Value{cty.NullVal(cty.Bool), cty.True}, 84 | cty.True, 85 | }, 86 | { 87 | []cty.Value{cty.NullVal(cty.Bool), cty.False}, 88 | cty.False, 89 | }, 90 | { 91 | []cty.Value{cty.NullVal(cty.Bool), cty.False, cty.StringVal("hello")}, 92 | cty.StringVal("false"), 93 | }, 94 | { 95 | []cty.Value{cty.True, cty.UnknownVal(cty.Bool)}, 96 | cty.True, 97 | }, 98 | { 99 | []cty.Value{cty.UnknownVal(cty.Bool), cty.True}, 100 | cty.UnknownVal(cty.Bool).RefineNotNull(), 101 | }, 102 | { 103 | []cty.Value{cty.UnknownVal(cty.Bool), cty.StringVal("hello")}, 104 | cty.UnknownVal(cty.String).RefineNotNull(), 105 | }, 106 | { 107 | []cty.Value{cty.DynamicVal, cty.True}, 108 | cty.UnknownVal(cty.Bool).RefineNotNull(), 109 | }, 110 | { 111 | []cty.Value{cty.DynamicVal}, 112 | cty.DynamicVal, 113 | }, 114 | } 115 | 116 | for _, test := range tests { 117 | t.Run(fmt.Sprintf("Coalesce(%#v...)", test.Values), func(t *testing.T) { 118 | got, err := Coalesce(test.Values...) 119 | 120 | if err != nil { 121 | t.Fatalf("unexpected error: %s", err) 122 | } 123 | 124 | if !got.RawEquals(test.Want) { 125 | t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) 126 | } 127 | }) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /cty/function/stdlib/regexp_test.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/zclconf/go-cty/cty" 8 | ) 9 | 10 | func TestRegex(t *testing.T) { 11 | tests := []struct { 12 | Pattern cty.Value 13 | String cty.Value 14 | Want cty.Value 15 | }{ 16 | { 17 | cty.StringVal("[a-z]+"), 18 | cty.StringVal("135abc456def789"), 19 | cty.StringVal("abc"), 20 | }, 21 | { 22 | cty.StringVal("([0-9]*)([a-z]*)"), 23 | cty.StringVal("135abc456def"), 24 | cty.TupleVal([]cty.Value{ 25 | cty.StringVal("135"), 26 | cty.StringVal("abc"), 27 | }), 28 | }, 29 | { 30 | cty.StringVal(`^(?:(?P[^:/?#]+):)?(?://(?P[^/?#]*))?(?P[^?#]*)(?:\?(?P[^#]*))?(?:#(?P.*))?`), 31 | cty.StringVal("http://www.ics.uci.edu/pub/ietf/uri/#Related"), 32 | cty.ObjectVal(map[string]cty.Value{ 33 | "scheme": cty.StringVal("http"), 34 | "authority": cty.StringVal("www.ics.uci.edu"), 35 | "path": cty.StringVal("/pub/ietf/uri/"), 36 | "query": cty.NullVal(cty.String), // query portion isn't present at all, because there's no ? 37 | "fragment": cty.StringVal("Related"), 38 | }), 39 | }, 40 | { 41 | cty.StringVal("([0-9]*)([a-z]*)"), 42 | cty.UnknownVal(cty.String), 43 | cty.UnknownVal(cty.Tuple([]cty.Type{ 44 | cty.String, 45 | cty.String, 46 | })).RefineNotNull(), 47 | }, 48 | { 49 | cty.StringVal("(?P[0-9]*)"), 50 | cty.UnknownVal(cty.String), 51 | cty.UnknownVal(cty.Object(map[string]cty.Type{ 52 | "num": cty.String, 53 | })).RefineNotNull(), 54 | }, 55 | { 56 | cty.UnknownVal(cty.String), 57 | cty.StringVal("135abc456def"), 58 | cty.DynamicVal, 59 | }, 60 | { 61 | cty.StringVal("[a-z]+").Mark(1), 62 | cty.StringVal("135abc456def789"), 63 | cty.StringVal("abc").Mark(1), 64 | }, 65 | { 66 | cty.StringVal("[a-z]+"), 67 | cty.StringVal("135abc456def789").Mark(2), 68 | cty.StringVal("abc").Mark(2), 69 | }, 70 | } 71 | 72 | for _, test := range tests { 73 | t.Run(fmt.Sprintf("Regex(%#v, %#v)", test.Pattern, test.String), func(t *testing.T) { 74 | got, err := Regex(test.Pattern, test.String) 75 | 76 | if err != nil { 77 | t.Fatalf("unexpected error: %s", err) 78 | } 79 | 80 | if !got.RawEquals(test.Want) { 81 | t.Errorf( 82 | "wrong result\npattern: %#v\nstring: %#v\ngot: %#v\nwant: %#v", 83 | test.Pattern, test.String, got, test.Want, 84 | ) 85 | } 86 | }) 87 | } 88 | } 89 | 90 | func TestRegexAll(t *testing.T) { 91 | tests := []struct { 92 | Pattern cty.Value 93 | String cty.Value 94 | Want cty.Value 95 | }{ 96 | { 97 | cty.StringVal("[a-z]+"), 98 | cty.StringVal("135abc456def789"), 99 | cty.ListVal([]cty.Value{ 100 | cty.StringVal("abc"), 101 | cty.StringVal("def"), 102 | }), 103 | }, 104 | { 105 | cty.StringVal("([0-9]*)([a-z]*)"), 106 | cty.StringVal("135abc456def"), 107 | cty.ListVal([]cty.Value{ 108 | cty.TupleVal([]cty.Value{ 109 | cty.StringVal("135"), 110 | cty.StringVal("abc"), 111 | }), 112 | cty.TupleVal([]cty.Value{ 113 | cty.StringVal("456"), 114 | cty.StringVal("def"), 115 | }), 116 | }), 117 | }, 118 | { 119 | cty.StringVal(`^(?:(?P[^:/?#]+):)?(?://(?P[^/?#]*))?(?P[^?#]*)(?:\?(?P[^#]*))?(?:#(?P.*))?`), 120 | cty.StringVal("http://www.ics.uci.edu/pub/ietf/uri/#Related"), 121 | cty.ListVal([]cty.Value{ 122 | cty.ObjectVal(map[string]cty.Value{ 123 | "scheme": cty.StringVal("http"), 124 | "authority": cty.StringVal("www.ics.uci.edu"), 125 | "path": cty.StringVal("/pub/ietf/uri/"), 126 | "query": cty.NullVal(cty.String), // query portion isn't present at all, because there's no ? 127 | "fragment": cty.StringVal("Related"), 128 | }), 129 | }), 130 | }, 131 | { 132 | cty.StringVal("([0-9]*)([a-z]*)"), 133 | cty.UnknownVal(cty.String), 134 | cty.UnknownVal(cty.List(cty.Tuple([]cty.Type{ 135 | cty.String, 136 | cty.String, 137 | }))).RefineNotNull(), 138 | }, 139 | { 140 | cty.StringVal("(?P[0-9]*)"), 141 | cty.UnknownVal(cty.String), 142 | cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{ 143 | "num": cty.String, 144 | }))).RefineNotNull(), 145 | }, 146 | { 147 | cty.UnknownVal(cty.String), 148 | cty.StringVal("135abc456def"), 149 | cty.UnknownVal(cty.List(cty.DynamicPseudoType)).RefineNotNull(), 150 | }, 151 | } 152 | 153 | for _, test := range tests { 154 | t.Run(fmt.Sprintf("RegexAll(%#v, %#v)", test.Pattern, test.String), func(t *testing.T) { 155 | got, err := RegexAll(test.Pattern, test.String) 156 | 157 | if err != nil { 158 | t.Fatalf("unexpected error: %s", err) 159 | } 160 | 161 | if !got.RawEquals(test.Want) { 162 | t.Errorf( 163 | "wrong result\npattern: %#v\nstring: %#v\ngot: %#v\nwant: %#v", 164 | test.Pattern, test.String, got, test.Want, 165 | ) 166 | } 167 | }) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /cty/function/stdlib/string_replace.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "github.com/zclconf/go-cty/cty" 8 | "github.com/zclconf/go-cty/cty/function" 9 | ) 10 | 11 | // ReplaceFunc is a function that searches a given string for another given 12 | // substring, and replaces each occurence with a given replacement string. 13 | // The substr argument is a simple string. 14 | var ReplaceFunc = function.New(&function.Spec{ 15 | Description: `Replaces all instances of the given substring in the given string with the given replacement string.`, 16 | Params: []function.Parameter{ 17 | { 18 | Name: "str", 19 | Description: `The string to search within.`, 20 | Type: cty.String, 21 | }, 22 | { 23 | Name: "substr", 24 | Description: `The substring to search for.`, 25 | Type: cty.String, 26 | }, 27 | { 28 | Name: "replace", 29 | Description: `The new substring to replace substr with.`, 30 | Type: cty.String, 31 | }, 32 | }, 33 | Type: function.StaticReturnType(cty.String), 34 | RefineResult: refineNonNull, 35 | Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 36 | str := args[0].AsString() 37 | substr := args[1].AsString() 38 | replace := args[2].AsString() 39 | 40 | return cty.StringVal(strings.Replace(str, substr, replace, -1)), nil 41 | }, 42 | }) 43 | 44 | // RegexReplaceFunc is a function that searches a given string for another 45 | // given substring, and replaces each occurence with a given replacement 46 | // string. The substr argument must be a valid regular expression. 47 | var RegexReplaceFunc = function.New(&function.Spec{ 48 | Description: `Applies the given regular expression pattern to the given string and replaces all matches with the given replacement string.`, 49 | Params: []function.Parameter{ 50 | { 51 | Name: "str", 52 | Type: cty.String, 53 | }, 54 | { 55 | Name: "pattern", 56 | Type: cty.String, 57 | }, 58 | { 59 | Name: "replace", 60 | Type: cty.String, 61 | }, 62 | }, 63 | Type: function.StaticReturnType(cty.String), 64 | RefineResult: refineNonNull, 65 | Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { 66 | str := args[0].AsString() 67 | substr := args[1].AsString() 68 | replace := args[2].AsString() 69 | 70 | re, err := regexp.Compile(substr) 71 | if err != nil { 72 | return cty.UnknownVal(cty.String), err 73 | } 74 | 75 | return cty.StringVal(re.ReplaceAllString(str, replace)), nil 76 | }, 77 | }) 78 | 79 | // Replace searches a given string for another given substring, 80 | // and replaces all occurrences with a given replacement string. 81 | func Replace(str, substr, replace cty.Value) (cty.Value, error) { 82 | return ReplaceFunc.Call([]cty.Value{str, substr, replace}) 83 | } 84 | 85 | func RegexReplace(str, substr, replace cty.Value) (cty.Value, error) { 86 | return RegexReplaceFunc.Call([]cty.Value{str, substr, replace}) 87 | } 88 | -------------------------------------------------------------------------------- /cty/function/stdlib/string_replace_test.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/zclconf/go-cty/cty" 7 | ) 8 | 9 | func TestReplace(t *testing.T) { 10 | tests := []struct { 11 | Input cty.Value 12 | Substr, Replace cty.Value 13 | Want cty.Value 14 | }{ 15 | { 16 | cty.StringVal("hello"), 17 | cty.StringVal("l"), 18 | cty.StringVal(""), 19 | cty.StringVal("heo"), 20 | }, 21 | { 22 | cty.StringVal("😸😸😸😾😾😾"), 23 | cty.StringVal("😾"), 24 | cty.StringVal("😸"), 25 | cty.StringVal("😸😸😸😸😸😸"), 26 | }, 27 | { 28 | cty.StringVal("😸😸😸😸😸😾"), 29 | cty.StringVal("😾"), 30 | cty.StringVal("😸"), 31 | cty.StringVal("😸😸😸😸😸😸"), 32 | }, 33 | } 34 | 35 | for _, test := range tests { 36 | t.Run(test.Input.GoString()+"_replace", func(t *testing.T) { 37 | got, err := Replace(test.Input, test.Substr, test.Replace) 38 | 39 | if err != nil { 40 | t.Fatalf("unexpected error: %s", err) 41 | } 42 | 43 | if !got.RawEquals(test.Want) { 44 | t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) 45 | } 46 | }) 47 | t.Run(test.Input.GoString()+"_regex_replace", func(t *testing.T) { 48 | got, err := Replace(test.Input, test.Substr, test.Replace) 49 | 50 | if err != nil { 51 | t.Fatalf("unexpected error: %s", err) 52 | } 53 | 54 | if !got.RawEquals(test.Want) { 55 | t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) 56 | } 57 | }) 58 | } 59 | } 60 | 61 | func TestRegexReplace(t *testing.T) { 62 | tests := []struct { 63 | Input cty.Value 64 | Substr, Replace cty.Value 65 | Want cty.Value 66 | }{ 67 | { 68 | cty.StringVal("-ab-axxb-"), 69 | cty.StringVal("a(x*)b"), 70 | cty.StringVal("T"), 71 | cty.StringVal("-T-T-"), 72 | }, 73 | { 74 | cty.StringVal("-ab-axxb-"), 75 | cty.StringVal("a(x*)b"), 76 | cty.StringVal("${1}W"), 77 | cty.StringVal("-W-xxW-"), 78 | }, 79 | } 80 | 81 | for _, test := range tests { 82 | t.Run(test.Input.GoString(), func(t *testing.T) { 83 | got, err := RegexReplace(test.Input, test.Substr, test.Replace) 84 | 85 | if err != nil { 86 | t.Fatalf("unexpected error: %s", err) 87 | } 88 | 89 | if !got.RawEquals(test.Want) { 90 | t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) 91 | } 92 | }) 93 | } 94 | } 95 | 96 | func TestRegexReplaceInvalidRegex(t *testing.T) { 97 | _, err := RegexReplace(cty.StringVal(""), cty.StringVal("("), cty.StringVal("")) 98 | if err == nil { 99 | t.Fatal("expected an error") 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /cty/function/stdlib/testdata/bare.tmpl: -------------------------------------------------------------------------------- 1 | ${val} -------------------------------------------------------------------------------- /cty/function/stdlib/testdata/func.tmpl: -------------------------------------------------------------------------------- 1 | The items are ${join(", ", list)} -------------------------------------------------------------------------------- /cty/function/stdlib/testdata/hello.tmpl: -------------------------------------------------------------------------------- 1 | Hello, ${name}! -------------------------------------------------------------------------------- /cty/function/stdlib/testdata/hello.txt: -------------------------------------------------------------------------------- 1 | Hello World -------------------------------------------------------------------------------- /cty/function/stdlib/testdata/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zclconf/go-cty/3c2b6a0e21d8e541c2ff5b3825b27483fad0bd1b/cty/function/stdlib/testdata/icon.png -------------------------------------------------------------------------------- /cty/function/stdlib/testdata/list.tmpl: -------------------------------------------------------------------------------- 1 | %{ for x in list ~} 2 | - ${x} 3 | %{ endfor ~} 4 | -------------------------------------------------------------------------------- /cty/function/stdlib/testdata/recursive.tmpl: -------------------------------------------------------------------------------- 1 | ${templatefile("recursive.tmpl", {})} -------------------------------------------------------------------------------- /cty/function/unpredictable.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import ( 4 | "github.com/zclconf/go-cty/cty" 5 | ) 6 | 7 | // Unpredictable wraps a given function such that it retains the same arguments 8 | // and type checking behavior but will return an unknown value when called. 9 | // 10 | // It is recommended that most functions be "pure", which is to say that they 11 | // will always produce the same value given particular input. However, 12 | // sometimes it is necessary to offer functions whose behavior depends on 13 | // some external state, such as reading a file or determining the current time. 14 | // In such cases, an unpredictable wrapper might be used to stand in for 15 | // the function during some sort of prior "checking" phase in order to delay 16 | // the actual effect until later. 17 | // 18 | // While Unpredictable can support a function that isn't pure in its 19 | // implementation, it still expects a function to be pure in its type checking 20 | // behavior, except for the special case of returning cty.DynamicPseudoType 21 | // if it is not yet able to predict its return value based on current argument 22 | // information. 23 | func Unpredictable(f Function) Function { 24 | newSpec := *f.spec // shallow copy 25 | newSpec.Impl = unpredictableImpl 26 | return New(&newSpec) 27 | } 28 | 29 | func unpredictableImpl(args []cty.Value, retType cty.Type) (cty.Value, error) { 30 | return cty.UnknownVal(retType), nil 31 | } 32 | -------------------------------------------------------------------------------- /cty/function/unpredictable_test.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/zclconf/go-cty/cty" 7 | ) 8 | 9 | func TestUnpredictable(t *testing.T) { 10 | f := New(&Spec{ 11 | Params: []Parameter{ 12 | { 13 | Name: "fixed", 14 | Type: cty.Bool, 15 | }, 16 | }, 17 | VarParam: &Parameter{ 18 | Name: "variadic", 19 | Type: cty.String, 20 | }, 21 | Type: func(args []cty.Value) (cty.Type, error) { 22 | if len(args) == 1 { 23 | return cty.Bool, nil 24 | } else { 25 | return cty.String, nil 26 | } 27 | }, 28 | Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 29 | return cty.NullVal(retType), nil 30 | }, 31 | }) 32 | 33 | uf := Unpredictable(f) 34 | 35 | { 36 | predVal, err := f.Call([]cty.Value{cty.True}) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | if !predVal.RawEquals(cty.NullVal(cty.Bool)) { 41 | t.Fatal("wrong predictable result") 42 | } 43 | } 44 | 45 | t.Run("argument type error", func(t *testing.T) { 46 | _, err := uf.Call([]cty.Value{cty.StringVal("hello")}) 47 | if err == nil { 48 | t.Fatal("call successful; want error") 49 | } 50 | }) 51 | 52 | t.Run("type check 1", func(t *testing.T) { 53 | ty, err := uf.ReturnTypeForValues([]cty.Value{cty.True}) 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | if !ty.Equals(cty.Bool) { 58 | t.Errorf("wrong type %#v; want %#v", ty, cty.Bool) 59 | } 60 | }) 61 | 62 | t.Run("type check 2", func(t *testing.T) { 63 | ty, err := uf.ReturnTypeForValues([]cty.Value{cty.True, cty.StringVal("hello")}) 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | if !ty.Equals(cty.String) { 68 | t.Errorf("wrong type %#v; want %#v", ty, cty.String) 69 | } 70 | }) 71 | 72 | t.Run("call", func(t *testing.T) { 73 | v, err := uf.Call([]cty.Value{cty.True}) 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | if !v.RawEquals(cty.UnknownVal(cty.Bool)) { 78 | t.Errorf("wrong result %#v; want %#v", v, cty.UnknownVal(cty.Bool)) 79 | } 80 | }) 81 | 82 | } 83 | -------------------------------------------------------------------------------- /cty/gocty/doc.go: -------------------------------------------------------------------------------- 1 | // Package gocty deals with converting between cty Values and native go 2 | // values. 3 | // 4 | // It operates under a similar principle to the encoding/json and 5 | // encoding/xml packages in the standard library, using reflection to 6 | // populate native Go data structures from cty values and vice-versa. 7 | package gocty 8 | -------------------------------------------------------------------------------- /cty/gocty/helpers.go: -------------------------------------------------------------------------------- 1 | package gocty 2 | 3 | import ( 4 | "math/big" 5 | "reflect" 6 | 7 | "github.com/zclconf/go-cty/cty" 8 | "github.com/zclconf/go-cty/cty/set" 9 | ) 10 | 11 | var valueType = reflect.TypeOf(cty.Value{}) 12 | var typeType = reflect.TypeOf(cty.Type{}) 13 | 14 | var setType = reflect.TypeOf(set.Set[interface{}]{}) 15 | 16 | var bigFloatType = reflect.TypeOf(big.Float{}) 17 | var bigIntType = reflect.TypeOf(big.Int{}) 18 | 19 | var emptyInterfaceType = reflect.TypeOf(interface{}(nil)) 20 | 21 | var stringType = reflect.TypeOf("") 22 | 23 | // structTagIndices interrogates the fields of the given type (which must 24 | // be a struct type, or we'll panic) and returns a map from the cty 25 | // attribute names declared via struct tags to the indices of the 26 | // fields holding those tags. 27 | // 28 | // This function will panic if two fields within the struct are tagged with 29 | // the same cty attribute name. 30 | func structTagIndices(st reflect.Type) map[string]int { 31 | ct := st.NumField() 32 | ret := make(map[string]int, ct) 33 | 34 | for i := 0; i < ct; i++ { 35 | field := st.Field(i) 36 | attrName := field.Tag.Get("cty") 37 | if attrName != "" { 38 | ret[attrName] = i 39 | } 40 | } 41 | 42 | return ret 43 | } 44 | -------------------------------------------------------------------------------- /cty/gocty/type_implied.go: -------------------------------------------------------------------------------- 1 | package gocty 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/zclconf/go-cty/cty" 7 | ) 8 | 9 | // ImpliedType takes an arbitrary Go value (as an interface{}) and attempts 10 | // to find a suitable cty.Type instance that could be used for a conversion 11 | // with ToCtyValue. 12 | // 13 | // This allows -- for simple situations at least -- types to be defined just 14 | // once in Go and the cty types derived from the Go types, but in the process 15 | // it makes some assumptions that may be undesirable so applications are 16 | // encouraged to build their cty types directly if exacting control is 17 | // required. 18 | // 19 | // Not all Go types can be represented as cty types, so an error may be 20 | // returned which is usually considered to be a bug in the calling program. 21 | // In particular, ImpliedType will never use capsule types in its returned 22 | // type, because it cannot know the capsule types supported by the calling 23 | // program. 24 | func ImpliedType(gv interface{}) (cty.Type, error) { 25 | rt := reflect.TypeOf(gv) 26 | var path cty.Path 27 | return impliedType(rt, path) 28 | } 29 | 30 | func impliedType(rt reflect.Type, path cty.Path) (cty.Type, error) { 31 | switch rt.Kind() { 32 | 33 | case reflect.Ptr: 34 | return impliedType(rt.Elem(), path) 35 | 36 | // Primitive types 37 | case reflect.Bool: 38 | return cty.Bool, nil 39 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 40 | return cty.Number, nil 41 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 42 | return cty.Number, nil 43 | case reflect.Float32, reflect.Float64: 44 | return cty.Number, nil 45 | case reflect.String: 46 | return cty.String, nil 47 | 48 | // Collection types 49 | case reflect.Slice: 50 | path := append(path, cty.IndexStep{Key: cty.UnknownVal(cty.Number)}) 51 | ety, err := impliedType(rt.Elem(), path) 52 | if err != nil { 53 | return cty.NilType, err 54 | } 55 | return cty.List(ety), nil 56 | case reflect.Map: 57 | if !stringType.AssignableTo(rt.Key()) { 58 | return cty.NilType, path.NewErrorf("no cty.Type for %s (must have string keys)", rt) 59 | } 60 | path := append(path, cty.IndexStep{Key: cty.UnknownVal(cty.String)}) 61 | ety, err := impliedType(rt.Elem(), path) 62 | if err != nil { 63 | return cty.NilType, err 64 | } 65 | return cty.Map(ety), nil 66 | 67 | // Structural types 68 | case reflect.Struct: 69 | return impliedStructType(rt, path) 70 | 71 | default: 72 | return cty.NilType, path.NewErrorf("no cty.Type for %s", rt) 73 | } 74 | } 75 | 76 | func impliedStructType(rt reflect.Type, path cty.Path) (cty.Type, error) { 77 | if valueType.AssignableTo(rt) { 78 | // Special case: cty.Value represents cty.DynamicPseudoType, for 79 | // type conformance checking. 80 | return cty.DynamicPseudoType, nil 81 | } 82 | 83 | fieldIdxs := structTagIndices(rt) 84 | if len(fieldIdxs) == 0 { 85 | return cty.NilType, path.NewErrorf("no cty.Type for %s (no cty field tags)", rt) 86 | } 87 | 88 | atys := make(map[string]cty.Type, len(fieldIdxs)) 89 | 90 | { 91 | // Temporary extension of path for attributes 92 | path := append(path, nil) 93 | 94 | for k, fi := range fieldIdxs { 95 | path[len(path)-1] = cty.GetAttrStep{Name: k} 96 | 97 | ft := rt.Field(fi).Type 98 | aty, err := impliedType(ft, path) 99 | if err != nil { 100 | return cty.NilType, err 101 | } 102 | 103 | atys[k] = aty 104 | } 105 | } 106 | 107 | return cty.Object(atys), nil 108 | } 109 | -------------------------------------------------------------------------------- /cty/gocty/type_implied_test.go: -------------------------------------------------------------------------------- 1 | package gocty 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/zclconf/go-cty/cty" 8 | ) 9 | 10 | func TestImpliedType(t *testing.T) { 11 | tests := []struct { 12 | Input interface{} 13 | Want cty.Type 14 | }{ 15 | // Primitive types 16 | { 17 | int(0), 18 | cty.Number, 19 | }, 20 | { 21 | int8(0), 22 | cty.Number, 23 | }, 24 | { 25 | int16(0), 26 | cty.Number, 27 | }, 28 | { 29 | int32(0), 30 | cty.Number, 31 | }, 32 | { 33 | int64(0), 34 | cty.Number, 35 | }, 36 | { 37 | uint(0), 38 | cty.Number, 39 | }, 40 | { 41 | uint8(0), 42 | cty.Number, 43 | }, 44 | { 45 | uint16(0), 46 | cty.Number, 47 | }, 48 | { 49 | uint32(0), 50 | cty.Number, 51 | }, 52 | { 53 | uint64(0), 54 | cty.Number, 55 | }, 56 | { 57 | float32(0), 58 | cty.Number, 59 | }, 60 | { 61 | float64(0), 62 | cty.Number, 63 | }, 64 | { 65 | false, 66 | cty.Bool, 67 | }, 68 | { 69 | "", 70 | cty.String, 71 | }, 72 | 73 | // Collection types 74 | { 75 | []int(nil), 76 | cty.List(cty.Number), 77 | }, 78 | { 79 | [][]int(nil), 80 | cty.List(cty.List(cty.Number)), 81 | }, 82 | { 83 | map[string]int(nil), 84 | cty.Map(cty.Number), 85 | }, 86 | { 87 | map[string]map[string]int(nil), 88 | cty.Map(cty.Map(cty.Number)), 89 | }, 90 | { 91 | map[string][]int(nil), 92 | cty.Map(cty.List(cty.Number)), 93 | }, 94 | 95 | // Structs 96 | { 97 | testStruct{}, 98 | cty.Object(map[string]cty.Type{ 99 | "name": cty.String, 100 | "number": cty.Number, 101 | }), 102 | }, 103 | 104 | // Pointers (unwrapped and ignored) 105 | { 106 | ptrToInt(0), 107 | cty.Number, 108 | }, 109 | { 110 | ptrToBool(false), 111 | cty.Bool, 112 | }, 113 | { 114 | ptrToString(""), 115 | cty.String, 116 | }, 117 | { 118 | &testStruct{}, 119 | cty.Object(map[string]cty.Type{ 120 | "name": cty.String, 121 | "number": cty.Number, 122 | }), 123 | }, 124 | 125 | // Dynamic 126 | { 127 | cty.NilVal, 128 | cty.DynamicPseudoType, 129 | }, 130 | } 131 | 132 | for _, test := range tests { 133 | t.Run(fmt.Sprintf("%#v", test.Input), func(t *testing.T) { 134 | got, err := ImpliedType(test.Input) 135 | if err != nil { 136 | t.Fatalf("unexpected error: %s", err) 137 | } 138 | 139 | if !got.Equals(test.Want) { 140 | t.Fatalf( 141 | "wrong result\ninput: %#v\ngot: %#v\nwant: %#v", 142 | test.Input, got, test.Want, 143 | ) 144 | } 145 | }) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /cty/helper.go: -------------------------------------------------------------------------------- 1 | package cty 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // anyUnknown is a helper to easily check if a set of values contains any 8 | // unknowns, for operations that short-circuit to return unknown in that case. 9 | func anyUnknown(values ...Value) bool { 10 | for _, val := range values { 11 | if _, unknown := val.v.(*unknownType); unknown { 12 | return true 13 | } 14 | } 15 | return false 16 | } 17 | 18 | // typeCheck tests whether all of the given values belong to the given type. 19 | // If the given types are a mixture of the given type and the dynamic 20 | // pseudo-type then a short-circuit dynamic value is returned. If the given 21 | // values are all of the correct type but at least one is unknown then 22 | // a short-circuit unknown value is returned. If any other types appear then 23 | // an error is returned. Otherwise (finally!) the result is nil, nil. 24 | func typeCheck(required Type, ret Type, values ...Value) (shortCircuit *Value, err error) { 25 | hasDynamic := false 26 | hasUnknown := false 27 | 28 | for i, val := range values { 29 | if val.ty == DynamicPseudoType { 30 | hasDynamic = true 31 | continue 32 | } 33 | 34 | if !val.Type().Equals(required) { 35 | return nil, fmt.Errorf( 36 | "type mismatch: want %s but value %d is %s", 37 | required.FriendlyName(), 38 | i, val.ty.FriendlyName(), 39 | ) 40 | } 41 | 42 | if _, unknown := val.v.(*unknownType); unknown { 43 | hasUnknown = true 44 | } 45 | } 46 | 47 | if hasDynamic { 48 | return &DynamicVal, nil 49 | } 50 | 51 | if hasUnknown { 52 | ret := UnknownVal(ret) 53 | return &ret, nil 54 | } 55 | 56 | return nil, nil 57 | } 58 | 59 | // mustTypeCheck is a wrapper around typeCheck that immediately panics if 60 | // any error is returned. 61 | func mustTypeCheck(required Type, ret Type, values ...Value) *Value { 62 | shortCircuit, err := typeCheck(required, ret, values...) 63 | if err != nil { 64 | panic(err) 65 | } 66 | return shortCircuit 67 | } 68 | 69 | // shortCircuitForceType takes the return value from mustTypeCheck and 70 | // replaces it with an unknown of the given type if the original value was 71 | // DynamicVal. 72 | // 73 | // This is useful for operations that are specified to always return a 74 | // particular type, since then a dynamic result can safely be "upgrade" to 75 | // a strongly-typed unknown, which then allows subsequent operations to 76 | // be actually type-checked. 77 | // 78 | // It is safe to use this only if the operation in question is defined as 79 | // returning either a value of the given type or panicking, since we know 80 | // then that subsequent operations won't run if the operation panics. 81 | // 82 | // If the given short-circuit value is *not* DynamicVal then it must be 83 | // of the given type, or this function will panic. 84 | func forceShortCircuitType(shortCircuit *Value, ty Type) *Value { 85 | if shortCircuit == nil { 86 | return nil 87 | } 88 | 89 | if shortCircuit.ty == DynamicPseudoType { 90 | ret := UnknownVal(ty) 91 | return &ret 92 | } 93 | 94 | if !shortCircuit.ty.Equals(ty) { 95 | panic("forceShortCircuitType got value of wrong type") 96 | } 97 | 98 | return shortCircuit 99 | } 100 | -------------------------------------------------------------------------------- /cty/json/doc.go: -------------------------------------------------------------------------------- 1 | // Package json provides functions for serializing cty types and values in 2 | // JSON format, and for decoding them again. 3 | // 4 | // Since the cty type system is a superset of the JSON type system, 5 | // round-tripping through JSON is lossy unless type information is provided 6 | // both at encoding time and decoding time. Callers of this package are 7 | // therefore suggested to define their expected structure as a cty.Type 8 | // and pass it in consistently both when encoding and when decoding, though 9 | // default (type-lossy) behavior is provided for situations where the precise 10 | // representation of the data is not significant. 11 | package json 12 | -------------------------------------------------------------------------------- /cty/json/simple.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "github.com/zclconf/go-cty/cty" 5 | ) 6 | 7 | // SimpleJSONValue is a wrapper around cty.Value that adds implementations of 8 | // json.Marshaler and json.Unmarshaler for simple-but-type-lossy automatic 9 | // encoding and decoding of values. 10 | // 11 | // The couplet Marshal and Unmarshal both take extra type information to 12 | // inform the encoding and decoding process so that all of the cty types 13 | // can be represented even though JSON's type system is a subset. 14 | // 15 | // SimpleJSONValue instead takes the approach of discarding the value's type 16 | // information and then deriving a new type from the stored structure when 17 | // decoding. This results in the same data being returned but not necessarily 18 | // with exactly the same type. 19 | // 20 | // For information on how types are inferred when decoding, see the 21 | // documentation of the function ImpliedType. 22 | type SimpleJSONValue struct { 23 | cty.Value 24 | } 25 | 26 | // MarshalJSON is an implementation of json.Marshaler. See the documentation 27 | // of SimpleJSONValue for more information. 28 | func (v SimpleJSONValue) MarshalJSON() ([]byte, error) { 29 | return Marshal(v.Value, v.Type()) 30 | } 31 | 32 | // UnmarshalJSON is an implementation of json.Unmarshaler. See the 33 | // documentation of SimpleJSONValue for more information. 34 | func (v *SimpleJSONValue) UnmarshalJSON(buf []byte) error { 35 | t, err := ImpliedType(buf) 36 | if err != nil { 37 | return err 38 | } 39 | v.Value, err = Unmarshal(buf, t) 40 | return err 41 | } 42 | -------------------------------------------------------------------------------- /cty/json/simple_test.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/zclconf/go-cty/cty" 8 | ) 9 | 10 | func TestSimpleJSONValue(t *testing.T) { 11 | tests := []struct { 12 | Input cty.Value 13 | JSON string 14 | Want cty.Value 15 | }{ 16 | { 17 | cty.NumberIntVal(5), 18 | `5`, 19 | cty.NumberIntVal(5), 20 | }, 21 | { 22 | cty.True, 23 | `true`, 24 | cty.True, 25 | }, 26 | { 27 | cty.StringVal("hello"), 28 | `"hello"`, 29 | cty.StringVal("hello"), 30 | }, 31 | { 32 | cty.TupleVal([]cty.Value{cty.StringVal("hello"), cty.True}), 33 | `["hello",true]`, 34 | cty.TupleVal([]cty.Value{cty.StringVal("hello"), cty.True}), 35 | }, 36 | { 37 | cty.ListVal([]cty.Value{cty.False, cty.True}), 38 | `[false,true]`, 39 | cty.TupleVal([]cty.Value{cty.False, cty.True}), 40 | }, 41 | { 42 | cty.SetVal([]cty.Value{cty.False, cty.True}), 43 | `[false,true]`, 44 | cty.TupleVal([]cty.Value{cty.False, cty.True}), 45 | }, 46 | { 47 | cty.ObjectVal(map[string]cty.Value{"true": cty.True, "greet": cty.StringVal("hello")}), 48 | `{"greet":"hello","true":true}`, 49 | cty.ObjectVal(map[string]cty.Value{"true": cty.True, "greet": cty.StringVal("hello")}), 50 | }, 51 | { 52 | cty.MapVal(map[string]cty.Value{"true": cty.True, "false": cty.False}), 53 | `{"false":false,"true":true}`, 54 | cty.ObjectVal(map[string]cty.Value{"true": cty.True, "false": cty.False}), 55 | }, 56 | { 57 | cty.NullVal(cty.Bool), 58 | `null`, 59 | cty.NullVal(cty.DynamicPseudoType), // type is lost in the round-trip 60 | }, 61 | } 62 | 63 | for _, test := range tests { 64 | t.Run(test.Input.GoString(), func(t *testing.T) { 65 | wrappedInput := SimpleJSONValue{test.Input} 66 | buf, err := json.Marshal(wrappedInput) 67 | if err != nil { 68 | t.Fatalf("unexpected error from json.Marshal: %s", err) 69 | } 70 | if string(buf) != test.JSON { 71 | t.Fatalf( 72 | "incorrect JSON\ninput: %#v\ngot: %s\nwant: %s", 73 | test.Input, buf, test.JSON, 74 | ) 75 | } 76 | 77 | var wrappedOutput SimpleJSONValue 78 | err = json.Unmarshal(buf, &wrappedOutput) 79 | if err != nil { 80 | t.Fatalf("unexpected error from json.Unmarshal: %s", err) 81 | } 82 | 83 | if !wrappedOutput.Value.RawEquals(test.Want) { 84 | t.Fatalf( 85 | "incorrect result\nJSON: %s\ngot: %#v\nwant: %#v", 86 | buf, wrappedOutput.Value, test.Want, 87 | ) 88 | } 89 | }) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /cty/json/type.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "github.com/zclconf/go-cty/cty" 5 | ) 6 | 7 | // MarshalType returns a JSON serialization of the given type. 8 | // 9 | // This is just a thin wrapper around t.MarshalJSON, for symmetry with 10 | // UnmarshalType. 11 | func MarshalType(t cty.Type) ([]byte, error) { 12 | return t.MarshalJSON() 13 | } 14 | 15 | // UnmarshalType decodes a JSON serialization of the given type as produced 16 | // by either Type.MarshalJSON or MarshalType. 17 | // 18 | // This is a convenience wrapper around Type.UnmarshalJSON. 19 | func UnmarshalType(buf []byte) (cty.Type, error) { 20 | var t cty.Type 21 | err := t.UnmarshalJSON(buf) 22 | return t, err 23 | } 24 | -------------------------------------------------------------------------------- /cty/json/type_implied_test.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/zclconf/go-cty/cty" 7 | ) 8 | 9 | func TestImpliedType(t *testing.T) { 10 | tests := []struct { 11 | Input string 12 | Want cty.Type 13 | }{ 14 | { 15 | "null", 16 | cty.DynamicPseudoType, 17 | }, 18 | { 19 | "1", 20 | cty.Number, 21 | }, 22 | { 23 | "1.2222222222222222222222222222222222", 24 | cty.Number, 25 | }, 26 | { 27 | "999999999999999999999999999999999999999999999999999999999999", 28 | cty.Number, 29 | }, 30 | { 31 | `""`, 32 | cty.String, 33 | }, 34 | { 35 | `"hello"`, 36 | cty.String, 37 | }, 38 | { 39 | "true", 40 | cty.Bool, 41 | }, 42 | { 43 | "false", 44 | cty.Bool, 45 | }, 46 | { 47 | "{}", 48 | cty.EmptyObject, 49 | }, 50 | { 51 | `{"true": true}`, 52 | cty.Object(map[string]cty.Type{ 53 | "true": cty.Bool, 54 | }), 55 | }, 56 | { 57 | `{"true": true, "name": "Ermintrude", "null": null}`, 58 | cty.Object(map[string]cty.Type{ 59 | "true": cty.Bool, 60 | "name": cty.String, 61 | "null": cty.DynamicPseudoType, 62 | }), 63 | }, 64 | { 65 | "[]", 66 | cty.EmptyTuple, 67 | }, 68 | { 69 | "[true, 1.2, null]", 70 | cty.Tuple([]cty.Type{cty.Bool, cty.Number, cty.DynamicPseudoType}), 71 | }, 72 | { 73 | `[[true], [1.2], [null]]`, 74 | cty.Tuple([]cty.Type{ 75 | cty.Tuple([]cty.Type{cty.Bool}), 76 | cty.Tuple([]cty.Type{cty.Number}), 77 | cty.Tuple([]cty.Type{cty.DynamicPseudoType}), 78 | }), 79 | }, 80 | { 81 | `[{"true": true}, {"name": "Ermintrude"}, {"null": null}]`, 82 | cty.Tuple([]cty.Type{ 83 | cty.Object(map[string]cty.Type{ 84 | "true": cty.Bool, 85 | }), 86 | cty.Object(map[string]cty.Type{ 87 | "name": cty.String, 88 | }), 89 | cty.Object(map[string]cty.Type{ 90 | "null": cty.DynamicPseudoType, 91 | }), 92 | }), 93 | }, 94 | { 95 | `{"a": "hello", "a": "world"}`, 96 | cty.Object(map[string]cty.Type{ 97 | "a": cty.String, 98 | }), 99 | }, 100 | } 101 | 102 | for _, test := range tests { 103 | t.Run(test.Input, func(t *testing.T) { 104 | got, err := ImpliedType([]byte(test.Input)) 105 | 106 | if err != nil { 107 | t.Fatalf("unexpected error: %s", err) 108 | } 109 | 110 | if !got.Equals(test.Want) { 111 | t.Errorf( 112 | "wrong type\ninput: %s\ngot: %#v\nwant: %#v", 113 | test.Input, got, test.Want, 114 | ) 115 | } 116 | }) 117 | } 118 | } 119 | 120 | func TestImpliedTypeErrors(t *testing.T) { 121 | tests := []struct { 122 | Input string 123 | WantError string 124 | }{ 125 | { 126 | `{"a": "hello", "a": true}`, 127 | `duplicate "a" property in JSON object`, 128 | }, 129 | { 130 | `{}boop`, 131 | `extraneous data after JSON object`, 132 | }, 133 | { 134 | `[!]`, 135 | `invalid character '!' looking for beginning of value`, 136 | }, 137 | { 138 | `[}`, 139 | `invalid character '}' looking for beginning of value`, 140 | }, 141 | { 142 | `{true: null}`, 143 | `invalid character 't'`, 144 | }, 145 | } 146 | 147 | for _, test := range tests { 148 | t.Run(test.Input, func(t *testing.T) { 149 | _, err := ImpliedType([]byte(test.Input)) 150 | if err == nil { 151 | t.Fatalf("unexpected success\nwant error: %s", err) 152 | } 153 | 154 | if got, want := err.Error(), test.WantError; got != want { 155 | t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) 156 | } 157 | }) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /cty/json/value.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/zclconf/go-cty/cty" 7 | "github.com/zclconf/go-cty/cty/convert" 8 | ) 9 | 10 | // Marshal produces a JSON representation of the given value that can later 11 | // be decoded into a value of the given type. 12 | // 13 | // A type is specified separately to allow for the given type to include 14 | // cty.DynamicPseudoType to represent situations where any type is permitted 15 | // and so type information must be included to allow recovery of the stored 16 | // structure when decoding. 17 | // 18 | // The given type will also be used to attempt automatic conversions of any 19 | // non-conformant types in the given value, although this will not always 20 | // be possible. If the value cannot be made to be conformant then an error is 21 | // returned, which may be a cty.PathError. 22 | // 23 | // Capsule-typed values can be marshalled, but with some caveats. Since 24 | // capsule values are compared by pointer equality, it is impossible to recover 25 | // a value that will compare equal to the original value. Additionally, 26 | // it's not possible to JSON-serialize the capsule type itself, so it's not 27 | // valid to use capsule types within parts of the value that are conformed to 28 | // cty.DynamicPseudoType. Otherwise, a capsule value can be used as long as 29 | // the encapsulated type itself is serializable with the Marshal function 30 | // in encoding/json. 31 | func Marshal(val cty.Value, t cty.Type) ([]byte, error) { 32 | errs := val.Type().TestConformance(t) 33 | if errs != nil { 34 | // Attempt a conversion 35 | var err error 36 | val, err = convert.Convert(val, t) 37 | if err != nil { 38 | return nil, err 39 | } 40 | } 41 | 42 | // From this point onward, val can be assumed to be conforming to t. 43 | 44 | buf := &bytes.Buffer{} 45 | var path cty.Path 46 | err := marshal(val, t, path, buf) 47 | 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | return buf.Bytes(), nil 53 | } 54 | 55 | // Unmarshal decodes a JSON representation of the given value into a cty Value 56 | // conforming to the given type. 57 | // 58 | // While decoding, type conversions will be done where possible to make 59 | // the result conformant even if the types given in JSON are not exactly 60 | // correct. If conversion isn't possible then an error is returned, which 61 | // may be a cty.PathError. 62 | func Unmarshal(buf []byte, t cty.Type) (cty.Value, error) { 63 | var path cty.Path 64 | return unmarshal(buf, t, path) 65 | } 66 | -------------------------------------------------------------------------------- /cty/json_test.go: -------------------------------------------------------------------------------- 1 | package cty 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | func TestTypeJSONable(t *testing.T) { 9 | tests := []struct { 10 | Type Type 11 | Want string 12 | }{ 13 | { 14 | String, 15 | `"string"`, 16 | }, 17 | { 18 | Number, 19 | `"number"`, 20 | }, 21 | { 22 | Bool, 23 | `"bool"`, 24 | }, 25 | { 26 | List(Bool), 27 | `["list","bool"]`, 28 | }, 29 | { 30 | Map(Bool), 31 | `["map","bool"]`, 32 | }, 33 | { 34 | Set(Bool), 35 | `["set","bool"]`, 36 | }, 37 | { 38 | List(Map(Bool)), 39 | `["list",["map","bool"]]`, 40 | }, 41 | { 42 | Tuple([]Type{Bool, String}), 43 | `["tuple",["bool","string"]]`, 44 | }, 45 | { 46 | Object(map[string]Type{"bool": Bool, "string": String}), 47 | `["object",{"bool":"bool","string":"string"}]`, 48 | }, 49 | { 50 | ObjectWithOptionalAttrs(map[string]Type{"bool": Bool, "string": String}, []string{"string", "bool"}), 51 | `["object",{"bool":"bool","string":"string"},["bool","string"]]`, 52 | }, 53 | { 54 | DynamicPseudoType, 55 | `"dynamic"`, 56 | }, 57 | } 58 | 59 | for _, test := range tests { 60 | t.Run(test.Type.GoString(), func(t *testing.T) { 61 | result, err := json.Marshal(test.Type) 62 | 63 | if err != nil { 64 | t.Fatalf("unexpected error from Marshal: %s", err) 65 | } 66 | 67 | resultStr := string(result) 68 | 69 | if resultStr != test.Want { 70 | t.Errorf( 71 | "wrong result\ntype: %#v\ngot: %s\nwant: %s", 72 | test.Type, resultStr, test.Want, 73 | ) 74 | } 75 | 76 | var ty Type 77 | err = json.Unmarshal(result, &ty) 78 | if err != nil { 79 | t.Fatalf("unexpected error from Unmarshal: %s", err) 80 | } 81 | 82 | if !ty.Equals(test.Type) { 83 | t.Errorf( 84 | "type did not unmarshal correctly\njson: %s\ngot: %#v\nwant: %#v", 85 | resultStr, ty, test.Type, 86 | ) 87 | } 88 | }) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /cty/list_type.go: -------------------------------------------------------------------------------- 1 | package cty 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // TypeList instances represent specific list types. Each distinct ElementType 8 | // creates a distinct, non-equal list type. 9 | type typeList struct { 10 | typeImplSigil 11 | ElementTypeT Type 12 | } 13 | 14 | // List creates a map type with the given element Type. 15 | // 16 | // List types are CollectionType implementations. 17 | func List(elem Type) Type { 18 | return Type{ 19 | typeList{ 20 | ElementTypeT: elem, 21 | }, 22 | } 23 | } 24 | 25 | // Equals returns true if the other Type is a list whose element type is 26 | // equal to that of the receiver. 27 | func (t typeList) Equals(other Type) bool { 28 | ot, isList := other.typeImpl.(typeList) 29 | if !isList { 30 | return false 31 | } 32 | 33 | return t.ElementTypeT.Equals(ot.ElementTypeT) 34 | } 35 | 36 | func (t typeList) FriendlyName(mode friendlyTypeNameMode) string { 37 | elemName := t.ElementTypeT.friendlyNameMode(mode) 38 | if mode == friendlyTypeConstraintName { 39 | if t.ElementTypeT == DynamicPseudoType { 40 | elemName = "any single type" 41 | } 42 | } 43 | return "list of " + elemName 44 | } 45 | 46 | func (t typeList) ElementType() Type { 47 | return t.ElementTypeT 48 | } 49 | 50 | func (t typeList) GoString() string { 51 | return fmt.Sprintf("cty.List(%#v)", t.ElementTypeT) 52 | } 53 | 54 | // IsListType returns true if the given type is a list type, regardless of its 55 | // element type. 56 | func (t Type) IsListType() bool { 57 | _, ok := t.typeImpl.(typeList) 58 | return ok 59 | } 60 | 61 | // ListElementType is a convenience method that checks if the given type is 62 | // a list type, returning a pointer to its element type if so and nil 63 | // otherwise. This is intended to allow convenient conditional branches, 64 | // like so: 65 | // 66 | // if et := t.ListElementType(); et != nil { 67 | // // Do something with *et 68 | // } 69 | func (t Type) ListElementType() *Type { 70 | if lt, ok := t.typeImpl.(typeList); ok { 71 | return <.ElementTypeT 72 | } 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /cty/map_type.go: -------------------------------------------------------------------------------- 1 | package cty 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // TypeList instances represent specific list types. Each distinct ElementType 8 | // creates a distinct, non-equal list type. 9 | type typeMap struct { 10 | typeImplSigil 11 | ElementTypeT Type 12 | } 13 | 14 | // Map creates a map type with the given element Type. 15 | // 16 | // Map types are CollectionType implementations. 17 | func Map(elem Type) Type { 18 | return Type{ 19 | typeMap{ 20 | ElementTypeT: elem, 21 | }, 22 | } 23 | } 24 | 25 | // Equals returns true if the other Type is a map whose element type is 26 | // equal to that of the receiver. 27 | func (t typeMap) Equals(other Type) bool { 28 | ot, isMap := other.typeImpl.(typeMap) 29 | if !isMap { 30 | return false 31 | } 32 | 33 | return t.ElementTypeT.Equals(ot.ElementTypeT) 34 | } 35 | 36 | func (t typeMap) FriendlyName(mode friendlyTypeNameMode) string { 37 | elemName := t.ElementTypeT.friendlyNameMode(mode) 38 | if mode == friendlyTypeConstraintName { 39 | if t.ElementTypeT == DynamicPseudoType { 40 | elemName = "any single type" 41 | } 42 | } 43 | return "map of " + elemName 44 | } 45 | 46 | func (t typeMap) ElementType() Type { 47 | return t.ElementTypeT 48 | } 49 | 50 | func (t typeMap) GoString() string { 51 | return fmt.Sprintf("cty.Map(%#v)", t.ElementTypeT) 52 | } 53 | 54 | // IsMapType returns true if the given type is a map type, regardless of its 55 | // element type. 56 | func (t Type) IsMapType() bool { 57 | _, ok := t.typeImpl.(typeMap) 58 | return ok 59 | } 60 | 61 | // MapElementType is a convenience method that checks if the given type is 62 | // a map type, returning a pointer to its element type if so and nil 63 | // otherwise. This is intended to allow convenient conditional branches, 64 | // like so: 65 | // 66 | // if et := t.MapElementType(); et != nil { 67 | // // Do something with *et 68 | // } 69 | func (t Type) MapElementType() *Type { 70 | if lt, ok := t.typeImpl.(typeMap); ok { 71 | return <.ElementTypeT 72 | } 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /cty/msgpack/doc.go: -------------------------------------------------------------------------------- 1 | // Package msgpack provides functions for serializing cty values in the 2 | // msgpack encoding, and decoding them again. 3 | // 4 | // If the same type information is provided both at encoding and decoding time 5 | // then values can be round-tripped without loss, except for capsule types 6 | // which are not currently supported. 7 | // 8 | // If any unknown values are passed to Marshal then they will be represented 9 | // using a msgpack extension with type code zero, which is understood by 10 | // the Unmarshal function within this package but will not be understood by 11 | // a generic (non-cty-aware) msgpack decoder. Ensure that no unknown values 12 | // are used if interoperability with other msgpack implementations is 13 | // required. 14 | package msgpack 15 | -------------------------------------------------------------------------------- /cty/msgpack/dynamic.go: -------------------------------------------------------------------------------- 1 | package msgpack 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/vmihailenco/msgpack/v5" 7 | "github.com/zclconf/go-cty/cty" 8 | ) 9 | 10 | type dynamicVal struct { 11 | Value cty.Value 12 | Path cty.Path 13 | } 14 | 15 | func (dv *dynamicVal) MarshalMsgpack() ([]byte, error) { 16 | // Rather than defining a msgpack-specific serialization of types, 17 | // instead we use the existing JSON serialization. 18 | typeJSON, err := dv.Value.Type().MarshalJSON() 19 | if err != nil { 20 | return nil, dv.Path.NewErrorf("failed to serialize type: %s", err) 21 | } 22 | var buf bytes.Buffer 23 | enc := msgpack.NewEncoder(&buf) 24 | enc.EncodeArrayLen(2) 25 | enc.EncodeBytes(typeJSON) 26 | err = marshal(dv.Value, dv.Value.Type(), dv.Path, enc) 27 | if err != nil { 28 | return nil, err 29 | } 30 | return buf.Bytes(), nil 31 | } 32 | -------------------------------------------------------------------------------- /cty/msgpack/infinity.go: -------------------------------------------------------------------------------- 1 | package msgpack 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | var negativeInfinity = math.Inf(-1) 8 | var positiveInfinity = math.Inf(1) 9 | -------------------------------------------------------------------------------- /cty/msgpack/type_implied.go: -------------------------------------------------------------------------------- 1 | package msgpack 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/vmihailenco/msgpack/v5" 9 | msgpackcodes "github.com/vmihailenco/msgpack/v5/msgpcode" 10 | "github.com/zclconf/go-cty/cty" 11 | ) 12 | 13 | // ImpliedType returns the cty Type implied by the structure of the given 14 | // msgpack-compliant buffer. This function implements the default type mapping 15 | // behavior used when decoding arbitrary msgpack without explicit cty Type 16 | // information. 17 | // 18 | // The rules are as follows: 19 | // 20 | // msgpack strings, numbers and bools map to their equivalent primitive type in 21 | // cty. 22 | // 23 | // msgpack maps become cty object types, with the attributes defined by the 24 | // map keys and the types of their values. 25 | // 26 | // msgpack arrays become cty tuple types, with the elements defined by the 27 | // types of the array members. 28 | // 29 | // Any nulls are typed as DynamicPseudoType, so callers of this function 30 | // must be prepared to deal with this. Callers that do not wish to deal with 31 | // dynamic typing should not use this function and should instead describe 32 | // their required types explicitly with a cty.Type instance when decoding. 33 | // 34 | // Any unknown values are similarly typed as DynamicPseudoType, because these 35 | // do not carry type information on the wire. 36 | // 37 | // Any parse errors will be returned as an error, and the type will be the 38 | // invalid value cty.NilType. 39 | func ImpliedType(buf []byte) (cty.Type, error) { 40 | r := bytes.NewReader(buf) 41 | dec := msgpack.NewDecoder(r) 42 | 43 | ty, err := impliedType(dec) 44 | if err != nil { 45 | return cty.NilType, err 46 | } 47 | 48 | // We must now be at the end of the buffer 49 | err = dec.Skip() 50 | if err != io.EOF { 51 | return ty, fmt.Errorf("extra bytes after msgpack value") 52 | } 53 | 54 | return ty, nil 55 | } 56 | 57 | func impliedType(dec *msgpack.Decoder) (cty.Type, error) { 58 | // If this function returns with a nil error then it must have already 59 | // consumed the next value from the decoder, since when called recursively 60 | // the caller will be expecting to find a following value here. 61 | 62 | code, err := dec.PeekCode() 63 | if err != nil { 64 | return cty.NilType, err 65 | } 66 | 67 | switch { 68 | 69 | case code == msgpackcodes.Nil || msgpackcodes.IsExt(code): 70 | err := dec.Skip() 71 | return cty.DynamicPseudoType, err 72 | 73 | case code == msgpackcodes.True || code == msgpackcodes.False: 74 | _, err := dec.DecodeBool() 75 | return cty.Bool, err 76 | 77 | case msgpackcodes.IsFixedNum(code): 78 | _, err := dec.DecodeInt64() 79 | return cty.Number, err 80 | 81 | case code == msgpackcodes.Int8 || code == msgpackcodes.Int16 || code == msgpackcodes.Int32 || code == msgpackcodes.Int64: 82 | _, err := dec.DecodeInt64() 83 | return cty.Number, err 84 | 85 | case code == msgpackcodes.Uint8 || code == msgpackcodes.Uint16 || code == msgpackcodes.Uint32 || code == msgpackcodes.Uint64: 86 | _, err := dec.DecodeUint64() 87 | return cty.Number, err 88 | 89 | case code == msgpackcodes.Float || code == msgpackcodes.Double: 90 | _, err := dec.DecodeFloat64() 91 | return cty.Number, err 92 | 93 | case msgpackcodes.IsString(code): 94 | _, err := dec.DecodeString() 95 | return cty.String, err 96 | 97 | case msgpackcodes.IsFixedMap(code) || code == msgpackcodes.Map16 || code == msgpackcodes.Map32: 98 | return impliedObjectType(dec) 99 | 100 | case msgpackcodes.IsFixedArray(code) || code == msgpackcodes.Array16 || code == msgpackcodes.Array32: 101 | return impliedTupleType(dec) 102 | 103 | default: 104 | return cty.NilType, fmt.Errorf("unsupported msgpack code %#v", code) 105 | } 106 | } 107 | 108 | func impliedObjectType(dec *msgpack.Decoder) (cty.Type, error) { 109 | // If we get in here then we've already peeked the next code and know 110 | // it's some sort of map. 111 | l, err := dec.DecodeMapLen() 112 | if err != nil { 113 | return cty.DynamicPseudoType, nil 114 | } 115 | 116 | var atys map[string]cty.Type 117 | 118 | for i := 0; i < l; i++ { 119 | // Read the map key first. We require maps to be strings, but msgpack 120 | // doesn't so we're prepared to error here if not. 121 | k, err := dec.DecodeString() 122 | if err != nil { 123 | return cty.DynamicPseudoType, err 124 | } 125 | 126 | aty, err := impliedType(dec) 127 | if err != nil { 128 | return cty.DynamicPseudoType, err 129 | } 130 | 131 | if atys == nil { 132 | atys = make(map[string]cty.Type) 133 | } 134 | atys[k] = aty 135 | } 136 | 137 | if len(atys) == 0 { 138 | return cty.EmptyObject, nil 139 | } 140 | 141 | return cty.Object(atys), nil 142 | } 143 | 144 | func impliedTupleType(dec *msgpack.Decoder) (cty.Type, error) { 145 | // If we get in here then we've already peeked the next code and know 146 | // it's some sort of array. 147 | l, err := dec.DecodeArrayLen() 148 | if err != nil { 149 | return cty.DynamicPseudoType, nil 150 | } 151 | 152 | if l == 0 { 153 | return cty.EmptyTuple, nil 154 | } 155 | 156 | etys := make([]cty.Type, l) 157 | 158 | for i := 0; i < l; i++ { 159 | ety, err := impliedType(dec) 160 | if err != nil { 161 | return cty.DynamicPseudoType, err 162 | } 163 | etys[i] = ety 164 | } 165 | 166 | return cty.Tuple(etys), nil 167 | } 168 | -------------------------------------------------------------------------------- /cty/msgpack/type_implied_test.go: -------------------------------------------------------------------------------- 1 | package msgpack 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/zclconf/go-cty/cty" 8 | ) 9 | 10 | func TestImpliedType(t *testing.T) { 11 | tests := []struct { 12 | Input string 13 | Want cty.Type 14 | }{ 15 | { 16 | "\xc0", 17 | cty.DynamicPseudoType, 18 | }, 19 | { 20 | "\x01", // positive fixnum 21 | cty.Number, 22 | }, 23 | { 24 | "\xff", // negative fixnum 25 | cty.Number, 26 | }, 27 | { 28 | "\xcc\x04", // uint8 29 | cty.Number, 30 | }, 31 | { 32 | "\xcd\x00\x04", // uint16 33 | cty.Number, 34 | }, 35 | { 36 | "\xce\x00\x04\x02\x01", // uint32 37 | cty.Number, 38 | }, 39 | { 40 | "\xcf\x00\x04\x02\x01\x00\x04\x02\x01", // uint64 41 | cty.Number, 42 | }, 43 | { 44 | "\xd0\x04", // int8 45 | cty.Number, 46 | }, 47 | { 48 | "\xd1\x00\x04", // int16 49 | cty.Number, 50 | }, 51 | { 52 | "\xd2\x00\x04\x02\x01", // int32 53 | cty.Number, 54 | }, 55 | { 56 | "\xd3\x00\x04\x02\x01\x00\x04\x02\x01", // int64 57 | cty.Number, 58 | }, 59 | { 60 | "\xca\x01\x01\x01\x01", // float32 61 | cty.Number, 62 | }, 63 | { 64 | "\xcb\x01\x01\x01\x01\x01\x01\x01\x01", // float64 65 | cty.Number, 66 | }, 67 | { 68 | "\xd4\x00\x00", // fixext1 (unknown value) 69 | cty.DynamicPseudoType, 70 | }, 71 | { 72 | "\xd5\x00\x00\x00", // fixext2 (unknown value) 73 | cty.DynamicPseudoType, 74 | }, 75 | { 76 | "\xa0", // fixstr (length zero) 77 | cty.String, 78 | }, 79 | { 80 | "\xa1\xff", // fixstr (length one) 81 | cty.String, 82 | }, 83 | { 84 | "\xd9\x00", // str8 (length zero) 85 | cty.String, 86 | }, 87 | { 88 | "\xd9\x01\xff", // str8 (length one) 89 | cty.String, 90 | }, 91 | { 92 | "\xda\x00\x00", // str16 (length zero) 93 | cty.String, 94 | }, 95 | { 96 | "\xda\x00\x01\xff", // str16 (length one) 97 | cty.String, 98 | }, 99 | { 100 | "\xdb\x00\x00\x00\x00", // str32 (length zero) 101 | cty.String, 102 | }, 103 | { 104 | "\xdb\x00\x00\x00\x01\xff", // str32 (length one) 105 | cty.String, 106 | }, 107 | { 108 | "\xc2", // false 109 | cty.Bool, 110 | }, 111 | { 112 | "\xc3", // true 113 | cty.Bool, 114 | }, 115 | { 116 | "\x90", // fixarray (length zero) 117 | cty.EmptyTuple, 118 | }, 119 | { 120 | "\x91\xa0", // fixarray (length one, element is empty string) 121 | cty.Tuple([]cty.Type{cty.String}), 122 | }, 123 | { 124 | "\xdc\x00\x00", // array16 (length zero) 125 | cty.EmptyTuple, 126 | }, 127 | { 128 | "\xdc\x00\x01\xc2", // array16 (length one, element is bool) 129 | cty.Tuple([]cty.Type{cty.Bool}), 130 | }, 131 | { 132 | "\xdd\x00\x00\x00\x00", // array32 (length zero) 133 | cty.EmptyTuple, 134 | }, 135 | { 136 | "\xdd\x00\x00\x00\x01\xc2", // array32 (length one, element is bool) 137 | cty.Tuple([]cty.Type{cty.Bool}), 138 | }, 139 | { 140 | "\x80", // fixmap (length zero) 141 | cty.EmptyObject, 142 | }, 143 | { 144 | "\x81\xa1a\xc2", // fixmap (length one, "a" => bool) 145 | cty.Object(map[string]cty.Type{"a": cty.Bool}), 146 | }, 147 | { 148 | "\xde\x00\x00", // map16 (length zero) 149 | cty.EmptyObject, 150 | }, 151 | { 152 | "\xde\x00\x01\xa1a\xc2", // map16 (length one, "a" => bool) 153 | cty.Object(map[string]cty.Type{"a": cty.Bool}), 154 | }, 155 | { 156 | "\xdf\x00\x00\x00\x00", // map32 (length zero) 157 | cty.EmptyObject, 158 | }, 159 | { 160 | "\xdf\x00\x00\x00\x01\xa1a\xc2", // map32 (length one, "a" => bool) 161 | cty.Object(map[string]cty.Type{"a": cty.Bool}), 162 | }, 163 | } 164 | 165 | for _, test := range tests { 166 | t.Run(fmt.Sprintf("%x", test.Input), func(t *testing.T) { 167 | got, err := ImpliedType([]byte(test.Input)) 168 | 169 | if err != nil { 170 | t.Fatalf("unexpected error: %s", err) 171 | } 172 | 173 | if !got.Equals(test.Want) { 174 | t.Errorf( 175 | "wrong type\ninput: %q\ngot: %#v\nwant: %#v", 176 | test.Input, got, test.Want, 177 | ) 178 | } 179 | }) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /cty/null.go: -------------------------------------------------------------------------------- 1 | package cty 2 | 3 | // NullVal returns a null value of the given type. A null can be created of any 4 | // type, but operations on such values will always panic. Calling applications 5 | // are encouraged to use nulls only sparingly, particularly when user-provided 6 | // expressions are to be evaluated, since the precence of nulls creates a 7 | // much higher chance of evaluation errors that can't be caught by a type 8 | // checker. 9 | func NullVal(t Type) Value { 10 | return Value{ 11 | ty: t, 12 | v: nil, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /cty/object_type_test.go: -------------------------------------------------------------------------------- 1 | package cty 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestObjectTypeEquals(t *testing.T) { 9 | tests := []struct { 10 | LHS Type // Must be typeObject 11 | RHS Type 12 | Expected bool 13 | }{ 14 | { 15 | Object(map[string]Type{}), 16 | Object(map[string]Type{}), 17 | true, 18 | }, 19 | { 20 | Object(map[string]Type{ 21 | "name": String, 22 | }), 23 | Object(map[string]Type{ 24 | "name": String, 25 | }), 26 | true, 27 | }, 28 | { 29 | // Attribute names should be normalized 30 | Object(map[string]Type{ 31 | "h\u00e9llo": String, // precombined é 32 | }), 33 | Object(map[string]Type{ 34 | "he\u0301llo": String, // e with combining acute accent 35 | }), 36 | true, 37 | }, 38 | { 39 | Object(map[string]Type{ 40 | "person": Object(map[string]Type{ 41 | "name": String, 42 | }), 43 | }), 44 | Object(map[string]Type{ 45 | "person": Object(map[string]Type{ 46 | "name": String, 47 | }), 48 | }), 49 | true, 50 | }, 51 | { 52 | Object(map[string]Type{ 53 | "name": String, 54 | }), 55 | Object(map[string]Type{}), 56 | false, 57 | }, 58 | { 59 | Object(map[string]Type{ 60 | "name": String, 61 | }), 62 | Object(map[string]Type{ 63 | "name": Number, 64 | }), 65 | false, 66 | }, 67 | { 68 | Object(map[string]Type{ 69 | "name": String, 70 | }), 71 | Object(map[string]Type{ 72 | "nombre": String, 73 | }), 74 | false, 75 | }, 76 | { 77 | Object(map[string]Type{ 78 | "name": String, 79 | }), 80 | Object(map[string]Type{ 81 | "name": String, 82 | "age": Number, 83 | }), 84 | false, 85 | }, 86 | { 87 | Object(map[string]Type{ 88 | "person": Object(map[string]Type{ 89 | "name": String, 90 | }), 91 | }), 92 | Object(map[string]Type{ 93 | "person": Object(map[string]Type{ 94 | "name": String, 95 | "age": Number, 96 | }), 97 | }), 98 | false, 99 | }, 100 | { 101 | ObjectWithOptionalAttrs( 102 | map[string]Type{ 103 | "person": Bool, 104 | }, 105 | []string{"person"}, 106 | ), 107 | ObjectWithOptionalAttrs( 108 | map[string]Type{ 109 | "person": Bool, 110 | }, 111 | []string{"person"}, 112 | ), 113 | true, 114 | }, 115 | { 116 | Object(map[string]Type{ 117 | "person": Object(map[string]Type{ 118 | "name": String, 119 | }), 120 | }), 121 | ObjectWithOptionalAttrs( 122 | map[string]Type{ 123 | "person": Bool, 124 | }, 125 | []string{"person"}, 126 | ), 127 | false, 128 | }, 129 | { 130 | ObjectWithOptionalAttrs( 131 | map[string]Type{ 132 | "person": Bool, 133 | }, 134 | []string{"person"}, 135 | ), 136 | Object(map[string]Type{ 137 | "person": Object(map[string]Type{ 138 | "name": String, 139 | }), 140 | }), 141 | false, 142 | }, 143 | } 144 | 145 | for _, test := range tests { 146 | t.Run(fmt.Sprintf("%#v.Equals(%#v)", test.LHS, test.RHS), func(t *testing.T) { 147 | got := test.LHS.Equals(test.RHS) 148 | if got != test.Expected { 149 | t.Errorf("Equals returned %#v; want %#v", got, test.Expected) 150 | } 151 | }) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /cty/path_set_test.go: -------------------------------------------------------------------------------- 1 | package cty 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestPathSet(t *testing.T) { 9 | helloWorld := Path{ 10 | GetAttrStep{Name: "hello"}, 11 | GetAttrStep{Name: "world"}, 12 | } 13 | s := NewPathSet(helloWorld) 14 | 15 | if got, want := s.Has(helloWorld), true; got != want { 16 | t.Errorf("set does not have hello.world; should have it") 17 | } 18 | if got, want := s.Has(helloWorld[:1]), false; got != want { 19 | t.Errorf("set has hello; should not have it") 20 | } 21 | 22 | if got, want := s.List(), []Path{helloWorld}; !reflect.DeepEqual(got, want) { 23 | t.Errorf("wrong list result\ngot: %#v\nwant: %#v", got, want) 24 | } 25 | 26 | fooBarBaz := Path{ 27 | GetAttrStep{Name: "foo"}, 28 | IndexStep{Key: StringVal("bar")}, 29 | GetAttrStep{Name: "baz"}, 30 | } 31 | s.AddAllSteps(fooBarBaz) 32 | if got, want := s.Has(helloWorld), true; got != want { 33 | t.Errorf("set does not have hello.world; should have it") 34 | } 35 | if got, want := s.Has(fooBarBaz), true; got != want { 36 | t.Errorf("set does not have foo['bar'].baz; should have it") 37 | } 38 | if got, want := s.Has(fooBarBaz[:2]), true; got != want { 39 | t.Errorf("set does not have foo['bar']; should have it") 40 | } 41 | if got, want := s.Has(fooBarBaz[:1]), true; got != want { 42 | t.Errorf("set does not have foo; should have it") 43 | } 44 | 45 | s.Remove(fooBarBaz[:2]) 46 | if got, want := s.Has(fooBarBaz[:2]), false; got != want { 47 | t.Errorf("set has foo['bar']; should not have it") 48 | } 49 | if got, want := s.Has(fooBarBaz), true; got != want { 50 | t.Errorf("set does not have foo['bar'].baz; should have it") 51 | } 52 | if got, want := s.Has(fooBarBaz[:1]), true; got != want { 53 | t.Errorf("set does not have foo; should have it") 54 | } 55 | 56 | new := NewPathSet(s.List()...) 57 | if got, want := s.Equal(new), true; got != want { 58 | t.Errorf("new set does not equal original; want equal sets") 59 | } 60 | new.Remove(helloWorld) 61 | if got, want := s.Equal(new), false; got != want { 62 | t.Errorf("new set equals original; want non-equal sets") 63 | } 64 | new.Add(Path{ 65 | GetAttrStep{Name: "goodbye"}, 66 | GetAttrStep{Name: "world"}, 67 | }) 68 | if got, want := s.Equal(new), false; got != want { 69 | t.Errorf("new set equals original; want non-equal sets") 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /cty/primitive_type_test.go: -------------------------------------------------------------------------------- 1 | package cty 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestTypeIsPrimitiveType(t *testing.T) { 9 | tests := []struct { 10 | Type Type 11 | Want bool 12 | }{ 13 | {String, true}, 14 | {Number, true}, 15 | {Bool, true}, 16 | {DynamicPseudoType, false}, 17 | {List(String), false}, 18 | 19 | // Make sure our primitive constants are correctly constructed 20 | {True.Type(), true}, 21 | {False.Type(), true}, 22 | {Zero.Type(), true}, 23 | {PositiveInfinity.Type(), true}, 24 | {NegativeInfinity.Type(), true}, 25 | } 26 | 27 | for i, test := range tests { 28 | t.Run(fmt.Sprintf("%d %#v", i, test.Type), func(t *testing.T) { 29 | got := test.Type.IsPrimitiveType() 30 | if got != test.Want { 31 | t.Errorf( 32 | "wrong result\ntype: %#v\ngot: %#v\nwant: %#v", 33 | test.Type, 34 | test.Want, got, 35 | ) 36 | } 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /cty/set/iterator.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | type Iterator[T any] struct { 4 | vals []T 5 | idx int 6 | } 7 | 8 | func (it *Iterator[T]) Value() T { 9 | return it.vals[it.idx] 10 | } 11 | 12 | func (it *Iterator[T]) Next() bool { 13 | it.idx++ 14 | return it.idx < len(it.vals) 15 | } 16 | -------------------------------------------------------------------------------- /cty/set/rules.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | // Rules represents the operations that define membership for a Set. 4 | // 5 | // Each Set has a Rules instance, whose methods must satisfy the interface 6 | // contracts given below for any value that will be added to the set. 7 | type Rules[T any] interface { 8 | // Hash returns an int that somewhat-uniquely identifies the given value. 9 | // 10 | // A good hash function will minimize collisions for values that will be 11 | // added to the set, though collisions *are* permitted. Collisions will 12 | // simply reduce the efficiency of operations on the set. 13 | Hash(T) int 14 | 15 | // Equivalent returns true if and only if the two values are considered 16 | // equivalent for the sake of set membership. Two values that are 17 | // equivalent cannot exist in the set at the same time, and if two 18 | // equivalent values are added it is undefined which one will be 19 | // returned when enumerating all of the set members. 20 | // 21 | // Two values that are equivalent *must* result in the same hash value, 22 | // though it is *not* required that two values with the same hash value 23 | // be equivalent. 24 | Equivalent(T, T) bool 25 | 26 | // SameRules returns true if the instance is equivalent to another Rules 27 | // instance over the same element type. 28 | SameRules(Rules[T]) bool 29 | } 30 | 31 | // OrderedRules is an extension of Rules that can apply a partial order to 32 | // element values. When a set's Rules implements OrderedRules an iterator 33 | // over the set will return items in the order described by the rules. 34 | // 35 | // If the given order is not a total order (that is, some pairs of non-equivalent 36 | // elements do not have a defined order) then the resulting iteration order 37 | // is undefined but consistent for a particular version of cty. The exact 38 | // order in that case is not part of the contract and is subject to change 39 | // between versions. 40 | type OrderedRules[T any] interface { 41 | Rules[T] 42 | 43 | // Less returns true if and only if the first argument should sort before 44 | // the second argument. If the second argument should sort before the first 45 | // or if there is no defined order for the values, return false. 46 | Less(interface{}, interface{}) bool 47 | } 48 | -------------------------------------------------------------------------------- /cty/set/rules_test.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | // testRules is a rules implementation that is used for testing. It only 4 | // accepts ints as values, and it has a Hash function that just returns the 5 | // given value modulo 16 so that we can easily and dependably test the 6 | // situation where two non-equivalent values have the same hash value. 7 | type testRules struct{} 8 | 9 | func newTestRules() Rules[int] { 10 | return testRules{} 11 | } 12 | 13 | func (r testRules) Hash(val int) int { 14 | return val % 16 15 | } 16 | 17 | func (r testRules) Equivalent(val1 int, val2 int) bool { 18 | return val1 == val2 19 | } 20 | 21 | func (r testRules) SameRules(other Rules[int]) bool { 22 | // All testRules values are equal, so type-checking is enough. 23 | _, ok := other.(testRules) 24 | return ok 25 | } 26 | -------------------------------------------------------------------------------- /cty/set/set.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Set is an implementation of the concept of a set: a collection where all 8 | // values are conceptually either in or out of the set, but the members are 9 | // not ordered. 10 | // 11 | // This type primarily exists to be the internal type of sets in cty, but 12 | // it is considered to be at the same level of abstraction as Go's built in 13 | // slice and map collection types, and so should make no cty-specific 14 | // assumptions. 15 | // 16 | // Set operations are not thread safe. It is the caller's responsibility to 17 | // provide mutex guarantees where necessary. 18 | // 19 | // Set operations are not optimized to minimize memory pressure. Mutating 20 | // a set will generally create garbage and so should perhaps be avoided in 21 | // tight loops where memory pressure is a concern. 22 | type Set[T any] struct { 23 | vals map[int][]T 24 | rules Rules[T] 25 | } 26 | 27 | // NewSet returns an empty set with the membership rules given. 28 | func NewSet[T any](rules Rules[T]) Set[T] { 29 | return Set[T]{ 30 | vals: map[int][]T{}, 31 | rules: rules, 32 | } 33 | } 34 | 35 | func NewSetFromSlice[T any](rules Rules[T], vals []T) Set[T] { 36 | s := NewSet(rules) 37 | for _, v := range vals { 38 | s.Add(v) 39 | } 40 | return s 41 | } 42 | 43 | func sameRules[T any](s1 Set[T], s2 Set[T]) bool { 44 | return s1.rules.SameRules(s2.rules) 45 | } 46 | 47 | func mustHaveSameRules[T any](s1 Set[T], s2 Set[T]) { 48 | if !sameRules(s1, s2) { 49 | panic(fmt.Errorf("incompatible set rules: %#v, %#v", s1.rules, s2.rules)) 50 | } 51 | } 52 | 53 | // HasRules returns true if and only if the receiving set has the given rules 54 | // instance as its rules. 55 | func (s Set[T]) HasRules(rules Rules[T]) bool { 56 | return s.rules.SameRules(rules) 57 | } 58 | 59 | // Rules returns the receiving set's rules instance. 60 | func (s Set[T]) Rules() Rules[T] { 61 | return s.rules 62 | } 63 | -------------------------------------------------------------------------------- /cty/set_helper.go: -------------------------------------------------------------------------------- 1 | package cty 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/zclconf/go-cty/cty/set" 7 | ) 8 | 9 | // ValueSet is to cty.Set what []cty.Value is to cty.List and 10 | // map[string]cty.Value is to cty.Map. It's provided to allow callers a 11 | // convenient interface for manipulating sets before wrapping them in cty.Set 12 | // values using cty.SetValFromValueSet. 13 | // 14 | // Unlike value slices and value maps, ValueSet instances have a single 15 | // homogenous element type because that is a requirement of the underlying 16 | // set implementation, which uses the element type to select a suitable 17 | // hashing function. 18 | // 19 | // Set mutations are not concurrency-safe. 20 | type ValueSet struct { 21 | // ValueSet is just a thin wrapper around a set.Set with our value-oriented 22 | // "rules" applied. We do this so that the caller can work in terms of 23 | // cty.Value objects even though the set internals use the raw values. 24 | s set.Set[interface{}] 25 | } 26 | 27 | // NewValueSet creates and returns a new ValueSet with the given element type. 28 | func NewValueSet(ety Type) ValueSet { 29 | return newValueSet(set.NewSet(newSetRules(ety))) 30 | } 31 | 32 | func newValueSet(s set.Set[interface{}]) ValueSet { 33 | return ValueSet{ 34 | s: s, 35 | } 36 | } 37 | 38 | // ElementType returns the element type for the receiving ValueSet. 39 | func (s ValueSet) ElementType() Type { 40 | return s.s.Rules().(setRules).Type 41 | } 42 | 43 | // Add inserts the given value into the receiving set. 44 | func (s ValueSet) Add(v Value) { 45 | s.requireElementType(v) 46 | s.s.Add(v.v) 47 | } 48 | 49 | // Remove deletes the given value from the receiving set, if indeed it was 50 | // there in the first place. If the value is not present, this is a no-op. 51 | func (s ValueSet) Remove(v Value) { 52 | s.requireElementType(v) 53 | s.s.Remove(v.v) 54 | } 55 | 56 | // Has returns true if the given value is in the receiving set, or false if 57 | // it is not. 58 | func (s ValueSet) Has(v Value) bool { 59 | s.requireElementType(v) 60 | return s.s.Has(v.v) 61 | } 62 | 63 | // Copy performs a shallow copy of the receiving set, returning a new set 64 | // with the same rules and elements. 65 | func (s ValueSet) Copy() ValueSet { 66 | return newValueSet(s.s.Copy()) 67 | } 68 | 69 | // Length returns the number of values in the set. 70 | func (s ValueSet) Length() int { 71 | return s.s.Length() 72 | } 73 | 74 | // Values returns a slice of all of the values in the set in no particular 75 | // order. 76 | func (s ValueSet) Values() []Value { 77 | l := s.s.Length() 78 | if l == 0 { 79 | return nil 80 | } 81 | ret := make([]Value, 0, l) 82 | ety := s.ElementType() 83 | for it := s.s.Iterator(); it.Next(); { 84 | ret = append(ret, Value{ 85 | ty: ety, 86 | v: it.Value(), 87 | }) 88 | } 89 | return ret 90 | } 91 | 92 | // Union returns a new set that contains all of the members of both the 93 | // receiving set and the given set. Both sets must have the same element type, 94 | // or else this function will panic. 95 | func (s ValueSet) Union(other ValueSet) ValueSet { 96 | return newValueSet(s.s.Union(other.s)) 97 | } 98 | 99 | // Intersection returns a new set that contains the values that both the 100 | // receiver and given sets have in common. Both sets must have the same element 101 | // type, or else this function will panic. 102 | func (s ValueSet) Intersection(other ValueSet) ValueSet { 103 | return newValueSet(s.s.Intersection(other.s)) 104 | } 105 | 106 | // Subtract returns a new set that contains all of the values from the receiver 107 | // that are not also in the given set. Both sets must have the same element 108 | // type, or else this function will panic. 109 | func (s ValueSet) Subtract(other ValueSet) ValueSet { 110 | return newValueSet(s.s.Subtract(other.s)) 111 | } 112 | 113 | // SymmetricDifference returns a new set that contains all of the values from 114 | // both the receiver and given sets, except those that both sets have in 115 | // common. Both sets must have the same element type, or else this function 116 | // will panic. 117 | func (s ValueSet) SymmetricDifference(other ValueSet) ValueSet { 118 | return newValueSet(s.s.SymmetricDifference(other.s)) 119 | } 120 | 121 | // requireElementType panics if the given value is not of the set's element type. 122 | // 123 | // It also panics if the given value is marked, because marked values cannot 124 | // be stored in sets. 125 | func (s ValueSet) requireElementType(v Value) { 126 | if v.IsMarked() { 127 | panic("cannot store marked value directly in a set (make the set itself unknown instead)") 128 | } 129 | if !v.Type().Equals(s.ElementType()) { 130 | panic(fmt.Errorf("attempt to use %#v value with set of %#v", v.Type(), s.ElementType())) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /cty/set_type.go: -------------------------------------------------------------------------------- 1 | package cty 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type typeSet struct { 8 | typeImplSigil 9 | ElementTypeT Type 10 | } 11 | 12 | // Set creates a set type with the given element Type. 13 | // 14 | // Set types are CollectionType implementations. 15 | func Set(elem Type) Type { 16 | return Type{ 17 | typeSet{ 18 | ElementTypeT: elem, 19 | }, 20 | } 21 | } 22 | 23 | // Equals returns true if the other Type is a set whose element type is 24 | // equal to that of the receiver. 25 | func (t typeSet) Equals(other Type) bool { 26 | ot, isSet := other.typeImpl.(typeSet) 27 | if !isSet { 28 | return false 29 | } 30 | 31 | return t.ElementTypeT.Equals(ot.ElementTypeT) 32 | } 33 | 34 | func (t typeSet) FriendlyName(mode friendlyTypeNameMode) string { 35 | elemName := t.ElementTypeT.friendlyNameMode(mode) 36 | if mode == friendlyTypeConstraintName { 37 | if t.ElementTypeT == DynamicPseudoType { 38 | elemName = "any single type" 39 | } 40 | } 41 | return "set of " + elemName 42 | } 43 | 44 | func (t typeSet) ElementType() Type { 45 | return t.ElementTypeT 46 | } 47 | 48 | func (t typeSet) GoString() string { 49 | return fmt.Sprintf("cty.Set(%#v)", t.ElementTypeT) 50 | } 51 | 52 | // IsSetType returns true if the given type is a list type, regardless of its 53 | // element type. 54 | func (t Type) IsSetType() bool { 55 | _, ok := t.typeImpl.(typeSet) 56 | return ok 57 | } 58 | 59 | // SetElementType is a convenience method that checks if the given type is 60 | // a set type, returning a pointer to its element type if so and nil 61 | // otherwise. This is intended to allow convenient conditional branches, 62 | // like so: 63 | // 64 | // if et := t.SetElementType(); et != nil { 65 | // // Do something with *et 66 | // } 67 | func (t Type) SetElementType() *Type { 68 | if lt, ok := t.typeImpl.(typeSet); ok { 69 | return <.ElementTypeT 70 | } 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /cty/set_type_test.go: -------------------------------------------------------------------------------- 1 | package cty 2 | 3 | import ( 4 | "reflect" 5 | "sort" 6 | "testing" 7 | 8 | "github.com/google/go-cmp/cmp" 9 | ) 10 | 11 | func TestSetOperations(t *testing.T) { 12 | // This test is for the mechanisms that allow a calling application to 13 | // implement set operations using the underlying set.Set type. This is 14 | // not expected to be a common case but is useful, for example, for 15 | // implementing the set-related functions in function/stdlib . 16 | 17 | s1 := SetVal([]Value{ 18 | StringVal("a"), 19 | StringVal("b"), 20 | StringVal("c"), 21 | }) 22 | s2 := SetVal([]Value{ 23 | StringVal("c"), 24 | StringVal("d"), 25 | StringVal("e"), 26 | }) 27 | 28 | s1r := s1.AsValueSet() 29 | s2r := s2.AsValueSet() 30 | s3r := s1r.Union(s2r) 31 | 32 | s3 := SetValFromValueSet(s3r) 33 | 34 | if got, want := s3.LengthInt(), 5; got != want { 35 | t.Errorf("wrong length %d; want %d", got, want) 36 | } 37 | 38 | for _, wantStr := range []string{"a", "b", "c", "d", "e"} { 39 | if got, want := s3.HasElement(StringVal(wantStr)), True; got != want { 40 | t.Errorf("missing element %q", wantStr) 41 | } 42 | } 43 | } 44 | 45 | func TestSetOfCapsuleType(t *testing.T) { 46 | type capsuleTypeForSetTests struct { 47 | name string 48 | } 49 | 50 | encapsulatedNames := func(vals []Value) []string { 51 | if len(vals) == 0 { 52 | return nil 53 | } 54 | ret := make([]string, len(vals)) 55 | for i, v := range vals { 56 | ret[i] = v.EncapsulatedValue().(*capsuleTypeForSetTests).name 57 | } 58 | sort.Strings(ret) 59 | return ret 60 | } 61 | 62 | typeWithHash := CapsuleWithOps("with hash function", reflect.TypeOf(capsuleTypeForSetTests{}), &CapsuleOps{ 63 | RawEquals: func(a, b interface{}) bool { 64 | return a.(*capsuleTypeForSetTests).name == b.(*capsuleTypeForSetTests).name 65 | }, 66 | HashKey: func(v interface{}) string { 67 | return v.(*capsuleTypeForSetTests).name 68 | }, 69 | }) 70 | typeWithoutHash := CapsuleWithOps("without hash function", reflect.TypeOf(capsuleTypeForSetTests{}), &CapsuleOps{ 71 | RawEquals: func(a, b interface{}) bool { 72 | return a.(*capsuleTypeForSetTests).name == b.(*capsuleTypeForSetTests).name 73 | }, 74 | }) 75 | typeWithoutEquals := Capsule("without hash function", reflect.TypeOf(capsuleTypeForSetTests{})) 76 | 77 | t.Run("with hash", func(t *testing.T) { 78 | // When we provide a hashing function the set implementation can 79 | // optimize its internal storage by spreading values over multiple 80 | // smaller buckets. 81 | v := SetVal([]Value{ 82 | CapsuleVal(typeWithHash, &capsuleTypeForSetTests{"a"}), 83 | CapsuleVal(typeWithHash, &capsuleTypeForSetTests{"b"}), 84 | CapsuleVal(typeWithHash, &capsuleTypeForSetTests{"a"}), 85 | CapsuleVal(typeWithHash, &capsuleTypeForSetTests{"c"}), 86 | }) 87 | got := encapsulatedNames(v.AsValueSlice()) 88 | want := []string{"a", "b", "c"} 89 | if diff := cmp.Diff(want, got); diff != "" { 90 | t.Errorf("wrong element names\n%s", diff) 91 | } 92 | }) 93 | t.Run("without hash", func(t *testing.T) { 94 | // When we don't provide a hashing function the outward behavior 95 | // should still be identical but the internal storage won't be 96 | // so efficient, due to everything living in one big bucket and 97 | // so we have to scan over all values to test if a particular 98 | // element is present. 99 | v := SetVal([]Value{ 100 | CapsuleVal(typeWithoutHash, &capsuleTypeForSetTests{"a"}), 101 | CapsuleVal(typeWithoutHash, &capsuleTypeForSetTests{"b"}), 102 | CapsuleVal(typeWithoutHash, &capsuleTypeForSetTests{"a"}), 103 | CapsuleVal(typeWithoutHash, &capsuleTypeForSetTests{"c"}), 104 | }) 105 | got := encapsulatedNames(v.AsValueSlice()) 106 | want := []string{"a", "b", "c"} 107 | if diff := cmp.Diff(want, got); diff != "" { 108 | t.Errorf("wrong element names\n%s", diff) 109 | } 110 | }) 111 | t.Run("without equals", func(t *testing.T) { 112 | // When we don't even have an equals function we can still store 113 | // values in the set but we will use the default capsule type 114 | // behavior of comparing by pointer equality. That means that 115 | // the name field doesn't coalesce anymore, but two instances 116 | // of this same d should. 117 | d := &capsuleTypeForSetTests{"d"} 118 | v := SetVal([]Value{ 119 | CapsuleVal(typeWithoutEquals, &capsuleTypeForSetTests{"a"}), 120 | CapsuleVal(typeWithoutEquals, &capsuleTypeForSetTests{"b"}), 121 | CapsuleVal(typeWithoutEquals, d), 122 | CapsuleVal(typeWithoutEquals, &capsuleTypeForSetTests{"a"}), 123 | CapsuleVal(typeWithoutEquals, &capsuleTypeForSetTests{"c"}), 124 | CapsuleVal(typeWithoutEquals, d), 125 | }) 126 | got := encapsulatedNames(v.AsValueSlice()) 127 | want := []string{"a", "a", "b", "c", "d"} 128 | if diff := cmp.Diff(want, got); diff != "" { 129 | t.Errorf("wrong element names\n%s", diff) 130 | } 131 | }) 132 | 133 | } 134 | -------------------------------------------------------------------------------- /cty/tuple_type.go: -------------------------------------------------------------------------------- 1 | package cty 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type typeTuple struct { 8 | typeImplSigil 9 | ElemTypes []Type 10 | } 11 | 12 | // Tuple creates a tuple type with the given element types. 13 | // 14 | // After a slice is passed to this function the caller must no longer access 15 | // the underlying array, since ownership is transferred to this library. 16 | func Tuple(elemTypes []Type) Type { 17 | return Type{ 18 | typeTuple{ 19 | ElemTypes: elemTypes, 20 | }, 21 | } 22 | } 23 | 24 | func (t typeTuple) Equals(other Type) bool { 25 | if ot, ok := other.typeImpl.(typeTuple); ok { 26 | if len(t.ElemTypes) != len(ot.ElemTypes) { 27 | // Fast path: if we don't have the same number of elements 28 | // then we can't possibly be equal. 29 | return false 30 | } 31 | 32 | for i, ty := range t.ElemTypes { 33 | oty := ot.ElemTypes[i] 34 | if !ok { 35 | return false 36 | } 37 | if !oty.Equals(ty) { 38 | return false 39 | } 40 | } 41 | 42 | return true 43 | } 44 | return false 45 | } 46 | 47 | func (t typeTuple) FriendlyName(mode friendlyTypeNameMode) string { 48 | // There isn't really a friendly way to write a tuple type due to its 49 | // complexity, so we'll just do something English-ish. Callers will 50 | // probably want to make some extra effort to avoid ever printing out 51 | // a tuple type FriendlyName in its entirety. For example, could 52 | // produce an error message by diffing two object types and saying 53 | // something like "Expected attribute foo to be string, but got number". 54 | // TODO: Finish this 55 | return "tuple" 56 | } 57 | 58 | func (t typeTuple) GoString() string { 59 | if len(t.ElemTypes) == 0 { 60 | return "cty.EmptyTuple" 61 | } 62 | return fmt.Sprintf("cty.Tuple(%#v)", t.ElemTypes) 63 | } 64 | 65 | // EmptyTuple is a shorthand for Tuple([]Type{}), to more easily talk about 66 | // the empty tuple type. 67 | var EmptyTuple Type 68 | 69 | // EmptyTupleVal is the only possible non-null, non-unknown value of type 70 | // EmptyTuple. 71 | var EmptyTupleVal Value 72 | 73 | func init() { 74 | EmptyTuple = Tuple([]Type{}) 75 | EmptyTupleVal = Value{ 76 | ty: EmptyTuple, 77 | v: []interface{}{}, 78 | } 79 | } 80 | 81 | // IsTupleType returns true if the given type is an object type, regardless 82 | // of its element type. 83 | func (t Type) IsTupleType() bool { 84 | _, ok := t.typeImpl.(typeTuple) 85 | return ok 86 | } 87 | 88 | // Length returns the number of elements of the receiving tuple type. 89 | // Will panic if the reciever isn't a tuple type; use IsTupleType to determine 90 | // whether this operation will succeed. 91 | func (t Type) Length() int { 92 | if ot, ok := t.typeImpl.(typeTuple); ok { 93 | return len(ot.ElemTypes) 94 | } 95 | panic("Length on non-tuple Type") 96 | } 97 | 98 | // TupleElementType returns the type of the element with the given index. Will 99 | // panic if the receiver is not a tuple type (use IsTupleType to confirm) 100 | // or if the index is out of range (use Length to confirm). 101 | func (t Type) TupleElementType(idx int) Type { 102 | if ot, ok := t.typeImpl.(typeTuple); ok { 103 | return ot.ElemTypes[idx] 104 | } 105 | panic("TupleElementType on non-tuple Type") 106 | } 107 | 108 | // TupleElementTypes returns a slice of the recieving tuple type's element 109 | // types. Will panic if the receiver is not a tuple type (use IsTupleType 110 | // to confirm). 111 | // 112 | // The returned slice is part of the internal state of the type, and is provided 113 | // for read access only. It is forbidden for any caller to modify the 114 | // underlying array. For many purposes the element-related methods of Value 115 | // are more appropriate and more convenient to use. 116 | func (t Type) TupleElementTypes() []Type { 117 | if ot, ok := t.typeImpl.(typeTuple); ok { 118 | return ot.ElemTypes 119 | } 120 | panic("TupleElementTypes on non-tuple Type") 121 | } 122 | -------------------------------------------------------------------------------- /cty/tuple_type_test.go: -------------------------------------------------------------------------------- 1 | package cty 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestTupleTypeEquals(t *testing.T) { 9 | tests := []struct { 10 | LHS Type // Must be typeTuple 11 | RHS Type 12 | Expected bool 13 | }{ 14 | { 15 | Tuple([]Type{}), 16 | Tuple([]Type{}), 17 | true, 18 | }, 19 | { 20 | EmptyTuple, 21 | Tuple([]Type{}), 22 | true, 23 | }, 24 | { 25 | Tuple([]Type{String}), 26 | Tuple([]Type{String}), 27 | true, 28 | }, 29 | { 30 | Tuple([]Type{Tuple([]Type{String})}), 31 | Tuple([]Type{Tuple([]Type{String})}), 32 | true, 33 | }, 34 | { 35 | Tuple([]Type{String}), 36 | EmptyTuple, 37 | false, 38 | }, 39 | { 40 | Tuple([]Type{String}), 41 | Tuple([]Type{Number}), 42 | false, 43 | }, 44 | { 45 | Tuple([]Type{String}), 46 | Tuple([]Type{String, Number}), 47 | false, 48 | }, 49 | { 50 | Tuple([]Type{String}), 51 | Tuple([]Type{Tuple([]Type{String})}), 52 | false, 53 | }, 54 | } 55 | 56 | for _, test := range tests { 57 | t.Run(fmt.Sprintf("%#v.Equals(%#v)", test.LHS, test.RHS), func(t *testing.T) { 58 | got := test.LHS.Equals(test.RHS) 59 | if got != test.Expected { 60 | t.Errorf("Equals returned %#v; want %#v", got, test.Expected) 61 | } 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /cty/type_conform.go: -------------------------------------------------------------------------------- 1 | package cty 2 | 3 | // TestConformance recursively walks the receiver and the given other type and 4 | // returns nil if the receiver *conforms* to the given type. 5 | // 6 | // Type conformance is similar to type equality but has one crucial difference: 7 | // PseudoTypeDynamic can be used within the given type to represent that 8 | // *any* type is allowed. 9 | // 10 | // If any non-conformities are found, the returned slice will be non-nil and 11 | // contain at least one error value. It will be nil if the type is entirely 12 | // conformant. 13 | // 14 | // Note that the special behavior of PseudoTypeDynamic is the *only* exception 15 | // to normal type equality. Calling applications may wish to apply their own 16 | // automatic conversion logic to the given data structure to create a more 17 | // liberal notion of conformance to a type. 18 | // 19 | // Returned errors are usually (but not always) PathError instances that 20 | // indicate where in the structure the error was found. If a returned error 21 | // is of that type then the error message is written for (English-speaking) 22 | // end-users working within the cty type system, not mentioning any Go-oriented 23 | // implementation details. 24 | func (t Type) TestConformance(other Type) []error { 25 | path := make(Path, 0) 26 | var errs []error 27 | testConformance(t, other, path, &errs) 28 | return errs 29 | } 30 | 31 | func testConformance(given Type, want Type, path Path, errs *[]error) { 32 | if want.Equals(DynamicPseudoType) { 33 | // anything goes! 34 | return 35 | } 36 | 37 | if given.Equals(want) { 38 | // Any equal types are always conformant 39 | return 40 | } 41 | 42 | // The remainder of this function is concerned with detecting 43 | // and reporting the specific non-conformance, since we wouldn't 44 | // have got here if the types were not divergent. 45 | // We treat compound structures as special so that we can report 46 | // specifically what is non-conforming, rather than simply returning 47 | // the entire type names and letting the user puzzle it out. 48 | 49 | if given.IsObjectType() && want.IsObjectType() { 50 | givenAttrs := given.AttributeTypes() 51 | wantAttrs := want.AttributeTypes() 52 | 53 | for k := range givenAttrs { 54 | if _, exists := wantAttrs[k]; !exists { 55 | *errs = append( 56 | *errs, 57 | errorf(path, "unsupported attribute %q", k), 58 | ) 59 | } 60 | } 61 | for k := range wantAttrs { 62 | if _, exists := givenAttrs[k]; !exists { 63 | *errs = append( 64 | *errs, 65 | errorf(path, "missing required attribute %q", k), 66 | ) 67 | } 68 | } 69 | 70 | path = append(path, nil) 71 | pathIdx := len(path) - 1 72 | 73 | for k, wantAttrType := range wantAttrs { 74 | if givenAttrType, exists := givenAttrs[k]; exists { 75 | path[pathIdx] = GetAttrStep{Name: k} 76 | testConformance(givenAttrType, wantAttrType, path, errs) 77 | } 78 | } 79 | 80 | path = path[0:pathIdx] 81 | 82 | return 83 | } 84 | 85 | if given.IsTupleType() && want.IsTupleType() { 86 | givenElems := given.TupleElementTypes() 87 | wantElems := want.TupleElementTypes() 88 | 89 | if len(givenElems) != len(wantElems) { 90 | *errs = append( 91 | *errs, 92 | errorf(path, "%d elements are required, but got %d", len(wantElems), len(givenElems)), 93 | ) 94 | return 95 | } 96 | 97 | path = append(path, nil) 98 | pathIdx := len(path) - 1 99 | 100 | for i, wantElemType := range wantElems { 101 | givenElemType := givenElems[i] 102 | path[pathIdx] = IndexStep{Key: NumberIntVal(int64(i))} 103 | testConformance(givenElemType, wantElemType, path, errs) 104 | } 105 | 106 | path = path[0:pathIdx] 107 | 108 | return 109 | } 110 | 111 | if given.IsListType() && want.IsListType() { 112 | path = append(path, IndexStep{Key: UnknownVal(Number)}) 113 | pathIdx := len(path) - 1 114 | testConformance(given.ElementType(), want.ElementType(), path, errs) 115 | path = path[0:pathIdx] 116 | return 117 | } 118 | 119 | if given.IsMapType() && want.IsMapType() { 120 | path = append(path, IndexStep{Key: UnknownVal(String)}) 121 | pathIdx := len(path) - 1 122 | testConformance(given.ElementType(), want.ElementType(), path, errs) 123 | path = path[0:pathIdx] 124 | return 125 | } 126 | 127 | if given.IsSetType() && want.IsSetType() { 128 | path = append(path, IndexStep{Key: UnknownVal(given.ElementType())}) 129 | pathIdx := len(path) - 1 130 | testConformance(given.ElementType(), want.ElementType(), path, errs) 131 | path = path[0:pathIdx] 132 | return 133 | } 134 | 135 | *errs = append( 136 | *errs, 137 | errorf(path, "%s required, but received %s", want.FriendlyName(), given.FriendlyName()), 138 | ) 139 | } 140 | -------------------------------------------------------------------------------- /cty/unknown.go: -------------------------------------------------------------------------------- 1 | package cty 2 | 3 | // unknownType is the placeholder type used for the sigil value representing 4 | // "Unknown", to make it unambigiously distinct from any other possible value. 5 | type unknownType struct { 6 | // refinement is an optional object which, if present, describes some 7 | // additional constraints we know about the range of real values this 8 | // unknown value could be a placeholder for. 9 | refinement unknownValRefinement 10 | } 11 | 12 | // totallyUnknown is the representation a a value we know nothing about at 13 | // all. Subsequent refinements of an unknown value will cause creation of 14 | // other values of unknownType that can represent additional constraints 15 | // on the unknown value, but all unknown values start as totally unknown 16 | // and we will also typically lose all unknown value refinements when 17 | // round-tripping through serialization formats. 18 | var totallyUnknown interface{} = &unknownType{} 19 | 20 | // UnknownVal returns an Value that represents an unknown value of the given 21 | // type. Unknown values can be used to represent a value that is 22 | // not yet known. Its meaning is undefined in cty, but it could be used by 23 | // an calling application to allow partial evaluation. 24 | // 25 | // Unknown values of any type can be created of any type. All operations on 26 | // Unknown values themselves return Unknown. 27 | func UnknownVal(t Type) Value { 28 | return Value{ 29 | ty: t, 30 | v: totallyUnknown, 31 | } 32 | } 33 | 34 | func (t unknownType) GoString() string { 35 | // This is the stringification of our internal unknown marker. The 36 | // stringification of the public representation of unknowns is in 37 | // Value.GoString. 38 | return "cty.unknown" 39 | } 40 | 41 | type pseudoTypeDynamic struct { 42 | typeImplSigil 43 | } 44 | 45 | // DynamicPseudoType represents the dynamic pseudo-type. 46 | // 47 | // This type can represent situations where a type is not yet known. Its 48 | // meaning is undefined in cty, but it could be used by a calling 49 | // application to allow expression type checking with some types not yet known. 50 | // For example, the application might optimistically permit any operation on 51 | // values of this type in type checking, allowing a partial type-check result, 52 | // and then repeat the check when more information is known to get the 53 | // final, concrete type. 54 | // 55 | // It is a pseudo-type because it is used only as a sigil to the calling 56 | // application. "Unknown" is the only valid value of this pseudo-type, so 57 | // operations on values of this type will always short-circuit as per 58 | // the rules for that special value. 59 | var DynamicPseudoType Type 60 | 61 | func (t pseudoTypeDynamic) Equals(other Type) bool { 62 | _, ok := other.typeImpl.(pseudoTypeDynamic) 63 | return ok 64 | } 65 | 66 | func (t pseudoTypeDynamic) FriendlyName(mode friendlyTypeNameMode) string { 67 | switch mode { 68 | case friendlyTypeConstraintName: 69 | return "any type" 70 | default: 71 | return "dynamic" 72 | } 73 | } 74 | 75 | func (t pseudoTypeDynamic) GoString() string { 76 | return "cty.DynamicPseudoType" 77 | } 78 | 79 | // DynamicVal is the only valid value of the pseudo-type dynamic. 80 | // This value can be used as a placeholder where a value or expression's 81 | // type and value are both unknown, thus allowing partial evaluation. See 82 | // the docs for DynamicPseudoType for more information. 83 | var DynamicVal Value 84 | 85 | func init() { 86 | DynamicPseudoType = Type{ 87 | pseudoTypeDynamic{}, 88 | } 89 | DynamicVal = Value{ 90 | ty: DynamicPseudoType, 91 | v: totallyUnknown, 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /cty/unknown_as_null.go: -------------------------------------------------------------------------------- 1 | package cty 2 | 3 | // UnknownAsNull returns a value of the same type as the given value but 4 | // with any unknown values (including nested values) replaced with null 5 | // values of the same type. 6 | // 7 | // This can be useful if a result is to be serialized in a format that can't 8 | // represent unknowns, such as JSON, as long as the caller does not need to 9 | // retain the unknown value information. 10 | func UnknownAsNull(val Value) Value { 11 | ty := val.Type() 12 | switch { 13 | case val.IsNull(): 14 | return val 15 | case !val.IsKnown(): 16 | return NullVal(ty) 17 | case ty.IsListType() || ty.IsTupleType() || ty.IsSetType(): 18 | length := val.LengthInt() 19 | if length == 0 { 20 | // If there are no elements then we can't have unknowns 21 | return val 22 | } 23 | vals := make([]Value, 0, length) 24 | it := val.ElementIterator() 25 | for it.Next() { 26 | _, v := it.Element() 27 | vals = append(vals, UnknownAsNull(v)) 28 | } 29 | switch { 30 | case ty.IsListType(): 31 | return ListVal(vals) 32 | case ty.IsTupleType(): 33 | return TupleVal(vals) 34 | default: 35 | return SetVal(vals) 36 | } 37 | case ty.IsMapType() || ty.IsObjectType(): 38 | var length int 39 | switch { 40 | case ty.IsMapType(): 41 | length = val.LengthInt() 42 | default: 43 | length = len(val.Type().AttributeTypes()) 44 | } 45 | if length == 0 { 46 | // If there are no elements then we can't have unknowns 47 | return val 48 | } 49 | vals := make(map[string]Value, length) 50 | it := val.ElementIterator() 51 | for it.Next() { 52 | k, v := it.Element() 53 | vals[k.AsString()] = UnknownAsNull(v) 54 | } 55 | switch { 56 | case ty.IsMapType(): 57 | return MapVal(vals) 58 | default: 59 | return ObjectVal(vals) 60 | } 61 | } 62 | 63 | return val 64 | } 65 | -------------------------------------------------------------------------------- /cty/unknown_as_null_test.go: -------------------------------------------------------------------------------- 1 | package cty 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestUnknownAsNull(t *testing.T) { 8 | tests := []struct { 9 | Input Value 10 | Want Value 11 | }{ 12 | { 13 | StringVal("hello"), 14 | StringVal("hello"), 15 | }, 16 | { 17 | NullVal(String), 18 | NullVal(String), 19 | }, 20 | { 21 | UnknownVal(String), 22 | NullVal(String), 23 | }, 24 | 25 | { 26 | NullVal(DynamicPseudoType), 27 | NullVal(DynamicPseudoType), 28 | }, 29 | { 30 | NullVal(Object(map[string]Type{"test": String})), 31 | NullVal(Object(map[string]Type{"test": String})), 32 | }, 33 | { 34 | DynamicVal, 35 | NullVal(DynamicPseudoType), 36 | }, 37 | 38 | { 39 | ListValEmpty(String), 40 | ListValEmpty(String), 41 | }, 42 | { 43 | ListVal([]Value{ 44 | StringVal("hello"), 45 | }), 46 | ListVal([]Value{ 47 | StringVal("hello"), 48 | }), 49 | }, 50 | { 51 | ListVal([]Value{ 52 | NullVal(String), 53 | }), 54 | ListVal([]Value{ 55 | NullVal(String), 56 | }), 57 | }, 58 | { 59 | ListVal([]Value{ 60 | UnknownVal(String), 61 | }), 62 | ListVal([]Value{ 63 | NullVal(String), 64 | }), 65 | }, 66 | 67 | { 68 | SetValEmpty(String), 69 | SetValEmpty(String), 70 | }, 71 | { 72 | SetVal([]Value{ 73 | StringVal("hello"), 74 | }), 75 | SetVal([]Value{ 76 | StringVal("hello"), 77 | }), 78 | }, 79 | { 80 | SetVal([]Value{ 81 | NullVal(String), 82 | }), 83 | SetVal([]Value{ 84 | NullVal(String), 85 | }), 86 | }, 87 | { 88 | SetVal([]Value{ 89 | UnknownVal(String), 90 | }), 91 | SetVal([]Value{ 92 | NullVal(String), 93 | }), 94 | }, 95 | 96 | { 97 | EmptyTupleVal, 98 | EmptyTupleVal, 99 | }, 100 | { 101 | TupleVal([]Value{ 102 | StringVal("hello"), 103 | }), 104 | TupleVal([]Value{ 105 | StringVal("hello"), 106 | }), 107 | }, 108 | { 109 | TupleVal([]Value{ 110 | NullVal(String), 111 | }), 112 | TupleVal([]Value{ 113 | NullVal(String), 114 | }), 115 | }, 116 | { 117 | TupleVal([]Value{ 118 | UnknownVal(String), 119 | }), 120 | TupleVal([]Value{ 121 | NullVal(String), 122 | }), 123 | }, 124 | 125 | { 126 | MapValEmpty(String), 127 | MapValEmpty(String), 128 | }, 129 | { 130 | MapVal(map[string]Value{ 131 | "greeting": StringVal("hello"), 132 | }), 133 | MapVal(map[string]Value{ 134 | "greeting": StringVal("hello"), 135 | }), 136 | }, 137 | { 138 | MapVal(map[string]Value{ 139 | "greeting": NullVal(String), 140 | }), 141 | MapVal(map[string]Value{ 142 | "greeting": NullVal(String), 143 | }), 144 | }, 145 | { 146 | MapVal(map[string]Value{ 147 | "greeting": UnknownVal(String), 148 | }), 149 | MapVal(map[string]Value{ 150 | "greeting": NullVal(String), 151 | }), 152 | }, 153 | 154 | { 155 | EmptyObjectVal, 156 | EmptyObjectVal, 157 | }, 158 | { 159 | ObjectVal(map[string]Value{ 160 | "greeting": StringVal("hello"), 161 | }), 162 | ObjectVal(map[string]Value{ 163 | "greeting": StringVal("hello"), 164 | }), 165 | }, 166 | { 167 | ObjectVal(map[string]Value{ 168 | "greeting": NullVal(String), 169 | }), 170 | ObjectVal(map[string]Value{ 171 | "greeting": NullVal(String), 172 | }), 173 | }, 174 | { 175 | ObjectVal(map[string]Value{ 176 | "greeting": UnknownVal(String), 177 | }), 178 | ObjectVal(map[string]Value{ 179 | "greeting": NullVal(String), 180 | }), 181 | }, 182 | } 183 | 184 | for _, test := range tests { 185 | t.Run(test.Input.GoString(), func(t *testing.T) { 186 | got := UnknownAsNull(test.Input) 187 | if !got.RawEquals(test.Want) { 188 | t.Errorf( 189 | "wrong result\ninput: %#v\ngot: %#v\nwant: %#v", 190 | test.Input, got, test.Want, 191 | ) 192 | } 193 | }) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /cty/value.go: -------------------------------------------------------------------------------- 1 | package cty 2 | 3 | // Value represents a value of a particular type, and is the interface by 4 | // which operations are executed on typed values. 5 | // 6 | // Value has two different classes of method. Operation methods stay entirely 7 | // within the type system (methods accept and return Value instances) and 8 | // are intended for use in implementing a language in terms of cty, while 9 | // integration methods either enter or leave the type system, working with 10 | // native Go values. Operation methods are guaranteed to support all of the 11 | // expected short-circuit behavior for unknown and dynamic values, while 12 | // integration methods may not. 13 | // 14 | // The philosophy for the operations API is that it's the caller's 15 | // responsibility to ensure that the given types and values satisfy the 16 | // specified invariants during a separate type check, so that the caller is 17 | // able to return errors to its user from the application's own perspective. 18 | // 19 | // Consequently the design of these methods assumes such checks have already 20 | // been done and panics if any invariants turn out not to be satisfied. These 21 | // panic errors are not intended to be handled, but rather indicate a bug in 22 | // the calling application that should be fixed with more checks prior to 23 | // executing operations. 24 | // 25 | // A related consequence of this philosophy is that no automatic type 26 | // conversions are done. If a method specifies that its argument must be 27 | // number then it's the caller's responsibility to do that conversion before 28 | // the call, thus allowing the application to have more constrained conversion 29 | // rules than are offered by the built-in converter where necessary. 30 | type Value struct { 31 | ty Type 32 | v interface{} 33 | } 34 | 35 | // Type returns the type of the value. 36 | func (val Value) Type() Type { 37 | return val.ty 38 | } 39 | 40 | // IsKnown returns true if the value is known. That is, if it is not 41 | // the result of the unknown value constructor Unknown(...), and is not 42 | // the result of an operation on another unknown value. 43 | // 44 | // Unknown values are only produced either directly or as a result of 45 | // operating on other unknown values, and so an application that never 46 | // introduces Unknown values can be guaranteed to never receive any either. 47 | func (val Value) IsKnown() bool { 48 | if val.IsMarked() { 49 | return val.unmarkForce().IsKnown() 50 | } 51 | _, unknown := val.v.(*unknownType) 52 | return !unknown 53 | } 54 | 55 | // IsNull returns true if the value is null. Values of any type can be 56 | // null, but any operations on a null value will panic. No operation ever 57 | // produces null, so an application that never introduces Null values can 58 | // be guaranteed to never receive any either. 59 | func (val Value) IsNull() bool { 60 | if val.IsMarked() { 61 | return val.unmarkForce().IsNull() 62 | } 63 | return val.v == nil 64 | } 65 | 66 | // NilVal is an invalid Value that can be used as a placeholder when returning 67 | // with an error from a function that returns (Value, error). 68 | // 69 | // NilVal is *not* a valid error and so no operations may be performed on it. 70 | // Any attempt to use it will result in a panic. 71 | // 72 | // This should not be confused with the idea of a Null value, as returned by 73 | // NullVal. NilVal is a nil within the *Go* type system, and is invalid in 74 | // the cty type system. Null values *do* exist in the cty type system. 75 | var NilVal = Value{ 76 | ty: Type{typeImpl: nil}, 77 | v: nil, 78 | } 79 | 80 | // IsWhollyKnown is an extension of IsKnown that also recursively checks 81 | // inside collections and structures to see if there are any nested unknown 82 | // values. 83 | func (val Value) IsWhollyKnown() bool { 84 | if val.IsMarked() { 85 | return val.unmarkForce().IsWhollyKnown() 86 | } 87 | 88 | if !val.IsKnown() { 89 | return false 90 | } 91 | 92 | if val.IsNull() { 93 | // Can't recurse into a null, so we're done 94 | return true 95 | } 96 | 97 | switch { 98 | case val.CanIterateElements(): 99 | for it := val.ElementIterator(); it.Next(); { 100 | _, ev := it.Element() 101 | if !ev.IsWhollyKnown() { 102 | return false 103 | } 104 | } 105 | return true 106 | default: 107 | return true 108 | } 109 | } 110 | 111 | // HasWhollyKnownType checks if the value is dynamic, or contains any nested 112 | // DynamicVal. This implies that both the value is not known, and the final 113 | // type may change. 114 | func (val Value) HasWhollyKnownType() bool { 115 | // a null dynamic type is known 116 | if val.IsNull() { 117 | return true 118 | } 119 | 120 | // an unknown DynamicPseudoType is a DynamicVal, but we don't want to 121 | // check that value for equality here, since this method is used within the 122 | // equality check. 123 | if !val.IsKnown() && val.ty == DynamicPseudoType { 124 | return false 125 | } 126 | 127 | if val.CanIterateElements() { 128 | // if the value is not known, then we can look directly at the internal 129 | // types 130 | if !val.IsKnown() { 131 | return !val.ty.HasDynamicTypes() 132 | } 133 | 134 | for it := val.ElementIterator(); it.Next(); { 135 | _, ev := it.Element() 136 | if !ev.HasWhollyKnownType() { 137 | return false 138 | } 139 | } 140 | } 141 | 142 | return true 143 | } 144 | -------------------------------------------------------------------------------- /docs/capsule-type-operations.md: -------------------------------------------------------------------------------- 1 | # Capsule Type Operation Definitions 2 | 3 | As described in [the main introduction to Capsule types](./types.md#capsule-types), 4 | by default a capsule type supports no operations apart from comparison by 5 | reference identity. 6 | 7 | However, there are certain operations that calling applications reasonably 8 | expect to be able to use generically across values of any type, e.g. as part of 9 | decoding arbitrary user input and preparing it for use. 10 | 11 | To support this, calling applications can optionally implement some additional 12 | operations for their capsule types. This is limited only to the subset of 13 | operations that, as noted above, are reasonable to expect to have available on 14 | values of any type. 15 | 16 | It does not include type-specialized operations like 17 | arithmetic; to perform such operations against capsule types, implement that 18 | logic in your calling application instead where you must presumably already 19 | be making specialized decisions based on value types. 20 | 21 | The following operations are implementable: 22 | 23 | * The `GoString` result for values of the type, so that values can be included 24 | in `fmt` operations using `%#v` along with other values and give a more 25 | useful result. 26 | 27 | To stay within `cty`'s conventions, the `GoString` result should generally 28 | represent a call to a value constructor function that would produce an 29 | equivalent value. 30 | 31 | * The `GoString` result for the type itself, for similar reasons. 32 | 33 | * Equality as an operation. It's unnecessary to implement this unless your 34 | capsule type represents a container for other `cty` values that would 35 | need to be recursively compared for equality. For simple capsule types, 36 | just implement raw equality and it will be used for both situations. 37 | 38 | * Raw equality as an integration method. This is commonly used in tests in 39 | order to take into account not only the value itself but its null-ness and 40 | unknown-ness. Because capsule types are primarily intended to be simple 41 | transports for opaque application values, in simple cases this integration 42 | method can just be a wrapper around whatever normal equality operation would 43 | apply to the wrapped type. 44 | 45 | * Conversion to and from the capsule type, using the `convert` package. Some 46 | applications use conversion as part of decoding user input in order to 47 | coerce user values into an expected type, in which case implementing 48 | conversions can make an application's capsule types participate in such 49 | coersions as needed. 50 | 51 | To implement one or more of these operations on a capsule type, construct it 52 | with `cty.CapsuleWithOps` instead of just `cty.Capsule`. 53 | 54 | The operation implementations are provided as function references within a 55 | struct value, and so those function references can potentially be closures 56 | referring to arguments passed to a type constructor function in order to 57 | implement parameterized capsule types. 58 | 59 | For more information on the available operations and the contract for 60 | implementing each one, see the documentation on the fields of `cty.CapsuleOps`. 61 | 62 | ## Extension Data 63 | 64 | In addition to the operations that `cty` packages use directly as described 65 | above, `CapsuleOps` includes extra function `ExtensionData` which can be used 66 | for application-specific extensions. 67 | 68 | It creates an extensible namespace using arbitrary comparable values of named 69 | types, where an application can define a key in its own package namespace and 70 | then use it to retrieve values from cooperating capsule types. 71 | 72 | For example, if there were an application package called `happyapp` which 73 | defined an extension key `Frob`, a cooperating capsule type might implement 74 | `ExtensionData` like this: 75 | 76 | ```go 77 | ExtensionData: func (key interface{}) interface{} { 78 | switch key { 79 | case happyapp.Frob: // a value of some type belonging to "happyapp" 80 | return "frob indeed" 81 | default: 82 | return nil 83 | } 84 | } 85 | ``` 86 | 87 | Package `happyapp` should define this `Frob` symbol as being of a named type 88 | belonging to its own package, so that the type can serve as a namespace: 89 | 90 | ```go 91 | type frobType int 92 | 93 | var Frob frobType 94 | ``` 95 | 96 | `cty` itself doesn't do anything special with `ExtensionData`, but there is a 97 | convenience method `CapsuleExtensionData` on `cty.Type` that can be used with 98 | capsule types to attempt to access extension data keys without first retrieving 99 | the `CapsuleOps` and checking if it implements `ExtensionData`: 100 | 101 | ```go 102 | // (in package happyapp) 103 | if frobValue, ok := givenType.CapsuleExtensionData(Frob).(string); ok { 104 | // If we get in here then the capsule type correctly handles Frob, 105 | // returning a string. If the capsule type does not handle Frob or 106 | // if it returns the wrong type then we'll ignore it. 107 | log.Printf("This capsule type has a Frob: %s", frobValue) 108 | } 109 | ``` 110 | 111 | `CapsuleExtensionData` will panic if called on a non-capsule type. 112 | -------------------------------------------------------------------------------- /docs/json.md: -------------------------------------------------------------------------------- 1 | # JSON serialization of `cty` values 2 | 3 | [The `json` package](https://godoc.org/github.com/zclconf/go-cty/cty/json) 4 | allows `cty` values to be serialized as JSON and decoded back into `cty` values. 5 | 6 | Since the `cty` type system is a superset of the JSON type system, two modes 7 | of operation are possible: 8 | 9 | The recommended approach is to define the intended `cty` data structure as 10 | a `cty.Type` -- possibly involving `cty.DynamicPseudoType` placeholders -- 11 | which then allows full recovery of the original values with correct type 12 | information, assuming that the same type description can be provided at 13 | decoding time. 14 | 15 | Alternatively, this package can decode an arbitrary JSON data structure into 16 | the corresponding `cty` types, which means that it is possible to serialize 17 | a `cty` value without type information and then decode into a value that 18 | contains the same data but possibly uses different types to represent 19 | that data. This allows direct integration with the standard library 20 | `encoding/json` package, at the expense of type-lossy deserialization. 21 | 22 | ## Type-preserving JSON Serialization 23 | 24 | The `Marshal` and `Unmarshal` functions together provide for type-preserving 25 | serialization and deserialization (respectively) of `cty` values. 26 | 27 | The pattern for using these functions is to define the intended `cty` type 28 | as a `cty.Type` instance and then pass an identical type as the second argument 29 | to both `Marshal` and `Unmarshal`. Assuming an identical type is used for both 30 | functions, it is guaranteed that values will round-trip through JSON 31 | serialization to produce a value of the same type. 32 | 33 | The `cty.Type` passed to `Unmarshal` is used as a hint to resolve ambiguities 34 | in the mapping to JSON. For example, `cty` list, set and tuple types all 35 | lower to JSON arrays, so additional type information is needed to decide 36 | which type to use when unmarshaling. 37 | 38 | The `cty.Type` passed to `Marshal` serves a more subtle purpose. Any 39 | `cty.DynamicPseudoType` placeholders in the type will cause extra type 40 | information to be saved in the JSON data structure, which is then used 41 | by `Unmarshal` to recover the original type. 42 | 43 | Type-preserving JSON serialization is able to serialize and deserialize 44 | capsule-typed values whose encapsulated Go types are JSON-serializable, except 45 | when those values are conformed to a `cty.DynamicPseudoType`. However, since 46 | capsule values compare by pointer equality, a decoded value will not be 47 | equal (as `cty` defines it) with the value that produced it. 48 | 49 | ## Type-lossy JSON Serialization 50 | 51 | If a given application does not need to exactly preserve the type of a value, 52 | the `SimpleJSONValue` type provides a simpler method for JSON serialization 53 | that works with the `encoding/json` package in Go's standard library. 54 | 55 | `SimpleJSONValue` is a wrapper struct around `cty.Value`, which can be 56 | embedded into another struct used with the standard library `Marshal` and 57 | `Unmarshal` functions: 58 | 59 | ```go 60 | type Example struct { 61 | Name string `json:"name"` 62 | Value SimpleJSONValue `json:"value"` 63 | } 64 | 65 | var example Example 66 | example.Name = "Ermintrude" 67 | example.Value = SimpleJSONValue{cty.NumberIntVal(43)} 68 | ``` 69 | 70 | Since no specific `cty` type is available when unmarshalling into 71 | `SimpleJSONValue`, a straightforward mapping is used: 72 | 73 | * JSON strings become `cty.String` values. 74 | * JSON numbers become `cty.Number` values. 75 | * JSON booleans become `cty.Bool` values. 76 | * JSON arrays become `cty.Tuple`-typed values whose element types are selected via this mapping. 77 | * JSON objects become `cty.Object`-typed values whose attribute types are selected via this mapping. 78 | * Any JSON `null` is mapped to `cty.NullVal(cty.DynamicPseudoType)`. 79 | 80 | The above mapping is unambiguous and lossless, so any valid JSON buffer can be 81 | decoded into an equally-expressive `cty` value, but the type may not exactly 82 | match that of the value used to produce the JSON buffer in the first place. 83 | -------------------------------------------------------------------------------- /docs/marks.md: -------------------------------------------------------------------------------- 1 | # Value Marks 2 | 3 | ---- 4 | 5 | **Note:** Marks are an optional feature that will not be needed in most 6 | applications. If your application never uses this API then you don't need to 7 | worry about encountering marked values, and you can ignore this document. 8 | 9 | ---- 10 | 11 | A `cty.Value` can optionally be _marked_, which causes it to carry around some 12 | additonal metadata along with its value. Marks are just normal Go values that 13 | are value to use as map keys, and are compared by equality. 14 | 15 | For example, an application might use marks to track the origin of a particular 16 | value in order to give better error messages, or to present the value in a 17 | different way in a UI. 18 | 19 | ```go 20 | // Use a named type for all marks, for namespacing purposes. 21 | type fromConfigType int 22 | var fromConfig fromConfigType 23 | 24 | return val.Mark(fromConfig) 25 | ``` 26 | 27 | ```go 28 | if val.HasMark(fromConfig) { 29 | // Maybe warn the user that the value is derived from configuration? 30 | // Or whatever makes sense for the calling application. 31 | } 32 | ``` 33 | 34 | When a value is marked, operation methods using that value will propagate the 35 | marks to any result values. That makes marks "infectious" in the sense that 36 | they propagate through operations and accumulate in the result automatically. 37 | 38 | However, marks cannot propagate automatically thruogh _integration_ methods, 39 | and so a calilng application is required to explicitly _unmark_ a value 40 | before using them: 41 | 42 | ```go 43 | val, valMarks := val.Unmark() 44 | // ...then use integration methods with val, 45 | // eventually producing a result that propgates 46 | // the marks: 47 | return result.WithMarks(valMarks) 48 | ``` 49 | 50 | ## Marked Values in Sets 51 | 52 | Sets present an interesting problem for marks because marks do not contribute 53 | to equality of two values and thus it would be possible in principle to add 54 | the same value to a given set twice with different marks. 55 | 56 | To avoid the complexity of tracking superset marks on a per-element basis, 57 | `cty` instead makes a compromise: sets can never contain marked values, and 58 | if any marked values are passed to `cty.SetVal` then they will be automatically 59 | unmarked and the superset of all marks applied to the resulting set as a whole. 60 | 61 | This is lossy about exactly which elements contributed which marks, but is 62 | conservative in the sense that any access to elements in the set will encounter 63 | the superset marks as expected. 64 | 65 | ## Marks Under Conversion 66 | 67 | The `cty/convert` package is aware of marks and will automatically propagate 68 | them through conversions. That includes nested values that are marked, which 69 | will be propagated to the corresponding nested value in the result if possible, 70 | or will be simplified to marks on a container where an exact propagation is not 71 | possible. 72 | 73 | ## Marks as Function Arguments 74 | 75 | The `cty/function` package is aware of marks and will, by default, 76 | automatically unmark all function arguments prior to calling a function and 77 | propagate the argument marks to the result value so that most functions do 78 | not need to worry about handling marks. 79 | 80 | A function may opt in to handling marks itself for a particular parameter by 81 | setting `AllowMarks: true` on the definition of that parameter. If a function 82 | opts in, it is therefore responsible for correctly propagating any marks onto 83 | its result. 84 | 85 | A function's `Type` implementation will receive automatically-unmarked values 86 | unless `AllowMarks` is set, which means that return-type checking alone will 87 | disregard any marks unless `AllowMarks` is set. Because type checking does not 88 | return a value, there is no way for type checking alone to communicate which 89 | marks it encountered during its work. 90 | 91 | If you're using marks in a use-case around obscuring sensitive values, beware 92 | that type checking of some functions could extract information without 93 | preserving the sensitivity mark. For example, if a string marked as sensitive 94 | were passed as the first argument to the stdlib `JSONDecode` function then 95 | type-checking of that function will betray the object property names inside 96 | that value as part of the inferred result type. 97 | 98 | ## Marks Under Serialization 99 | 100 | Marks cannot be represented in either the JSON nor the msgpack serializations 101 | of cty values, and so the `Marshal` functions for those will return errors 102 | if they encounter marked values. 103 | 104 | If you need to serialize values that might contain marks, you must explicitly 105 | unmark the whole data structure first (e.g. using `Value.UnmarkDeep`) and then 106 | decide what to do with those marks in order to ensure that if it makes sense 107 | to propagate them through the serialization then they will get represented 108 | somehow. 109 | 110 | ## Relationship to "Refinements" 111 | 112 | The idea of annotating a value with additional information has some overlap 113 | with the concept of [Refinements](refinements.md). However, the two have 114 | different purposes and so different design details and tradeoffs. 115 | 116 | For more details, see 117 | [the corresponding section in the Refinements documentation](refinements.md#relationship-to-marks). 118 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/zclconf/go-cty 2 | 3 | require ( 4 | github.com/apparentlymart/go-textseg/v15 v15.0.0 5 | github.com/google/go-cmp v0.3.1 6 | github.com/vmihailenco/msgpack/v5 v5.3.5 7 | golang.org/x/text v0.11.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 13 | ) 14 | 15 | go 1.18 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= 2 | github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= 7 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 10 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 11 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 12 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 13 | github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= 14 | github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= 15 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 16 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 17 | golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= 18 | golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 20 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 21 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 22 | --------------------------------------------------------------------------------