While cty
provides a representation of values within its own type system,
a calling application will inevitably need to eventually pass values
to a native Go API, using native Go types.
The gocty
package
aims to make conversions between cty
values and Go values as convenient as
possible, using an approach similar to that used by encoding/json
where
the reflect
package is used to define the desired structure using Go
native types.
Converting Go values to cty
values is the task of the ToCtyValue
function.
It takes an arbitrary Go value (as an interface{}
) and cty.Type
describing
the shape of the desired value.
The given type is used both as a conformance check and as a source of hints
to resolve ambiguities in the mapping from Go types. For example, it is valid
to convert a Go slice to both a cty
set and list types, and so the given
cty
type is used to indicate which is desired.
The errors generated by this function use terminology aimed at the developers of the calling application, since it's assumed that any problems encountered are bugs in the calling program and are thus "should never happen" cases.
Since unknown values cannot be represented natively in Go's type system, gocty
works only with known values. An error will be generated if a caller attempts
to convert an unknown value into a Go value.
Converting cty
values to Go values is done via the FromCtyValue
function.
In this case, the function mutates a particular Go value in place rather
than returning a new value, as is traditional from similar functions in
packages like encoding/json
.
The function must be given a non-nil pointer to the value that should be
populated. If the function succeeds without error then this target value has
been populated with data from the given cty
value.
Any errors returned are written with the target audience being the hypothetical
user that wrote whatever input was transformed into the given cty value, and
thus the terminology used is cty
type system terminology.
As a concrete example, consider converting a value into a Go int8
:
var val int8
err := gocty.FromCtyValue(value, &val)
There are a few different ways that this can fail:
-
If
value
is not acty.Number
value, the error message returned says "a number is required", assuming that this value came from user input and the user provided a value of the wrong type. -
If
value
is not an integer, or it's an integer outside of the range of anint8
, the error message says "must be a whole number between -128 and 127", again assuming that this was user input and that the target type here is an implied constraint on the value provided by the user.
As a consequence, it is valid and encouraged to convert arbitrary user-supplied values into concrete Go data structures as a concise way to express certain data validation constraints in a declarative way, and then return any error message verbatim to the end-user.
As well as straightforward mappings of primitive and collection types, gocty
can convert object and tuple values to and from values of Go struct
types.
For tuples, the target struct
must have exactly the same number of fields
as exist in the tuple, and the fields are used in the order they are defined
with no regard to their names or tags. A struct
used to decode a tuple must
have all public attributes. These constraints mean that generally-speaking
it will be hard to re-use existing application structs for this purpose, and
instead a specialized struct must be used to represent each tuple type. For
simple uses, a struct defined inline within a function can be used.
For objects, the mapping is more flexible. Field tags are used to express which struct fields correspond to each object attribute, as in the following example:
type Example struct {
Name string `cty:"name"`
Age int `cty:"age"`
}
For the mapping to be valid, there must be a one-to-one correspondence between object attributes and tagged struct fields. The presence or absence of attribute tags in the struct is used to define which attributes are valid, and so error messages will be generated for any extraneous or missing attributes. Additional fields may be present without tags, but all fields with tags must be public.
If parts of the cty
data structure have types that can't be known until
runtime, it is possible to leave these portions un-decoded for later
processing.
To achieve this, cty.DynamicPseudoType
is used in the type passed to the
two conversion functions, and at the corresponding place in the Go data
structure a cty.Value
object is placed. When converting from cty
to Go,
the portion of the value corresponding to the dynamic pseudo-type is
assigned directly to the cty.Value
object with no conversion,
so the calling program can then use the core cty
API to interact with it.
The converse is true for converting from Go to cty
: any valid cty.Value
object can be provided, and it will be included verbatim in the returned
cty.Value
.
type Thing struct {
Name string `cty:"name"`
ExtraData cty.Value `cty:"extra_data"`
}
thingType := cty.Object(map[string]cty.Type{
"name": cty.String,
"extra_data": cty.DynamicPseudoType,
})
thingVal := cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("Ermintrude"),
"extra_data": cty.NumberIntVal(12),
})
var thing Thing
err := gocty.FromCtyValue(thingVal, &thing)
// (error check)
fmt.Printf("extra_data is %s", thing.ExtraData.Type().FriendlyName())
// Prints: "extra_data is number"
Since capsule types encapsulate native Go values, their handling in gocty
is a simple wrapping and un-wrapping of the encapsulated value. The
encapsulated type and the type of the target value must match.
Since capsule values capture a pointer to the target value, it is possible to round-trip a pointer from a Go value into a capsule value and back to a Go value and recover the original pointer value, referring to the same in-memory object.
In simple cases it can be desirable to just write a simple type in Go and
use it immediately in conversions, without needing to separately write out a
corresponding cty.Type
expression.
The ImpliedType
function helps with this by trying to generate a reasonable
cty.Type
from a native Go value. Not all cty
types can be represented in
this way, but if the goal is a straightforward mapping to a convenient Go
data structure then this function is suitable.
The mapping is as follows:
- Go's int, uint and float types all map to
cty.Number
. - Go's bool type maps to
cty.Bool
- Go's string type maps to
cty.String
- Go slice types map to
cty
lists with the element type mapped per these rules. - Go maps with string keys map to
cty
maps with the element type mapped per these rules. - Go struct types are converted to
cty
object types using the struct tag convention described above and these mapping rules for each tagged field. - A Go value of type
cty.Value
maps tocty.DynamicPseudoType
, allowing for values whose precise type isn't known statically.
ImpliedType
considers only the Go type of the provided value, so it's valid
to pass a nil or zero value of the type. When passing nil
, be sure to convert
it to the target type, e.g. []int(nil)
.