Skip to content

Latest commit

 

History

History
174 lines (136 loc) · 7.42 KB

gocty.md

File metadata and controls

174 lines (136 loc) · 7.42 KB

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 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:

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 structs

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.

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.

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).