175 lines
7.4 KiB
Markdown
175 lines
7.4 KiB
Markdown
|
# Converting between Go and `cty` values
|
||
|
|
||
|
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](https://godoc.org/github.com/apparentlymart/go-cty/cty/gocty)
|
||
|
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.
|
||
|
|
||
|
## From Go to `cty`
|
||
|
|
||
|
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.
|
||
|
|
||
|
## From `cty` to Go
|
||
|
|
||
|
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`:
|
||
|
|
||
|
```go
|
||
|
var val int8
|
||
|
err := gocty.FromCtyValue(value, &val)
|
||
|
```
|
||
|
|
||
|
There are a few different ways that this can fail:
|
||
|
|
||
|
* If `value` is not a `cty.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
|
||
|
an `int8`, 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.
|
||
|
|
||
|
## Converting to and from `struct`s
|
||
|
|
||
|
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:
|
||
|
|
||
|
```go
|
||
|
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 absense 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.
|
||
|
|
||
|
## Dynamically-typed Values
|
||
|
|
||
|
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`.
|
||
|
|
||
|
```go
|
||
|
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"
|
||
|
```
|
||
|
|
||
|
## Conversion of Capsule Types
|
||
|
|
||
|
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.
|
||
|
|
||
|
## Implied `cty` Type of a Go value
|
||
|
|
||
|
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 to `cty.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)`.
|