549 lines
14 KiB
Go
549 lines
14 KiB
Go
|
package gocty
|
||
|
|
||
|
import (
|
||
|
"math/big"
|
||
|
"reflect"
|
||
|
|
||
|
"github.com/zclconf/go-cty/cty"
|
||
|
"github.com/zclconf/go-cty/cty/convert"
|
||
|
"github.com/zclconf/go-cty/cty/set"
|
||
|
)
|
||
|
|
||
|
// ToCtyValue produces a cty.Value from a Go value. The result will conform
|
||
|
// to the given type, or an error will be returned if this is not possible.
|
||
|
//
|
||
|
// The target type serves as a hint to resolve ambiguities in the mapping.
|
||
|
// For example, the Go type set.Set tells us that the value is a set but
|
||
|
// does not describe the set's element type. This also allows for convenient
|
||
|
// conversions, such as populating a set from a slice rather than having to
|
||
|
// first explicitly instantiate a set.Set.
|
||
|
//
|
||
|
// The audience of this function is assumed to be the developers of Go code
|
||
|
// that is integrating with cty, and thus the error messages it returns are
|
||
|
// presented from Go's perspective. These messages are thus not appropriate
|
||
|
// for display to end-users. An error returned from ToCtyValue represents a
|
||
|
// bug in the calling program, not user error.
|
||
|
func ToCtyValue(val interface{}, ty cty.Type) (cty.Value, error) {
|
||
|
// 'path' starts off as empty but will grow for each level of recursive
|
||
|
// call we make, so by the time toCtyValue returns it is likely to have
|
||
|
// unused capacity on the end of it, depending on how deeply-recursive
|
||
|
// the given Type is.
|
||
|
path := make(cty.Path, 0)
|
||
|
return toCtyValue(reflect.ValueOf(val), ty, path)
|
||
|
}
|
||
|
|
||
|
func toCtyValue(val reflect.Value, ty cty.Type, path cty.Path) (cty.Value, error) {
|
||
|
if val != (reflect.Value{}) && val.Type().AssignableTo(valueType) {
|
||
|
// If the source value is a cty.Value then we'll try to just pass
|
||
|
// through to the target type directly.
|
||
|
return toCtyPassthrough(val, ty, path)
|
||
|
}
|
||
|
|
||
|
switch ty {
|
||
|
case cty.Bool:
|
||
|
return toCtyBool(val, path)
|
||
|
case cty.Number:
|
||
|
return toCtyNumber(val, path)
|
||
|
case cty.String:
|
||
|
return toCtyString(val, path)
|
||
|
case cty.DynamicPseudoType:
|
||
|
return toCtyDynamic(val, path)
|
||
|
}
|
||
|
|
||
|
switch {
|
||
|
case ty.IsListType():
|
||
|
return toCtyList(val, ty.ElementType(), path)
|
||
|
case ty.IsMapType():
|
||
|
return toCtyMap(val, ty.ElementType(), path)
|
||
|
case ty.IsSetType():
|
||
|
return toCtySet(val, ty.ElementType(), path)
|
||
|
case ty.IsObjectType():
|
||
|
return toCtyObject(val, ty.AttributeTypes(), path)
|
||
|
case ty.IsTupleType():
|
||
|
return toCtyTuple(val, ty.TupleElementTypes(), path)
|
||
|
case ty.IsCapsuleType():
|
||
|
return toCtyCapsule(val, ty, path)
|
||
|
}
|
||
|
|
||
|
// We should never fall out here
|
||
|
return cty.NilVal, path.NewErrorf("unsupported target type %#v", ty)
|
||
|
}
|
||
|
|
||
|
func toCtyBool(val reflect.Value, path cty.Path) (cty.Value, error) {
|
||
|
if val = toCtyUnwrapPointer(val); !val.IsValid() {
|
||
|
return cty.NullVal(cty.Bool), nil
|
||
|
}
|
||
|
|
||
|
switch val.Kind() {
|
||
|
|
||
|
case reflect.Bool:
|
||
|
return cty.BoolVal(val.Bool()), nil
|
||
|
|
||
|
default:
|
||
|
return cty.NilVal, path.NewErrorf("can't convert Go %s to bool", val.Kind())
|
||
|
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
func toCtyNumber(val reflect.Value, path cty.Path) (cty.Value, error) {
|
||
|
if val = toCtyUnwrapPointer(val); !val.IsValid() {
|
||
|
return cty.NullVal(cty.Number), nil
|
||
|
}
|
||
|
|
||
|
switch val.Kind() {
|
||
|
|
||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||
|
return cty.NumberIntVal(val.Int()), nil
|
||
|
|
||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||
|
return cty.NumberUIntVal(val.Uint()), nil
|
||
|
|
||
|
case reflect.Float32, reflect.Float64:
|
||
|
return cty.NumberFloatVal(val.Float()), nil
|
||
|
|
||
|
case reflect.Struct:
|
||
|
if val.Type().AssignableTo(bigIntType) {
|
||
|
bigInt := val.Interface().(big.Int)
|
||
|
bigFloat := (&big.Float{}).SetInt(&bigInt)
|
||
|
val = reflect.ValueOf(*bigFloat)
|
||
|
}
|
||
|
|
||
|
if val.Type().AssignableTo(bigFloatType) {
|
||
|
bigFloat := val.Interface().(big.Float)
|
||
|
return cty.NumberVal(&bigFloat), nil
|
||
|
}
|
||
|
|
||
|
fallthrough
|
||
|
default:
|
||
|
return cty.NilVal, path.NewErrorf("can't convert Go %s to number", val.Kind())
|
||
|
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
func toCtyString(val reflect.Value, path cty.Path) (cty.Value, error) {
|
||
|
if val = toCtyUnwrapPointer(val); !val.IsValid() {
|
||
|
return cty.NullVal(cty.String), nil
|
||
|
}
|
||
|
|
||
|
switch val.Kind() {
|
||
|
|
||
|
case reflect.String:
|
||
|
return cty.StringVal(val.String()), nil
|
||
|
|
||
|
default:
|
||
|
return cty.NilVal, path.NewErrorf("can't convert Go %s to string", val.Kind())
|
||
|
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
func toCtyList(val reflect.Value, ety cty.Type, path cty.Path) (cty.Value, error) {
|
||
|
if val = toCtyUnwrapPointer(val); !val.IsValid() {
|
||
|
return cty.NullVal(cty.List(ety)), nil
|
||
|
}
|
||
|
|
||
|
switch val.Kind() {
|
||
|
|
||
|
case reflect.Slice:
|
||
|
if val.IsNil() {
|
||
|
return cty.NullVal(cty.List(ety)), nil
|
||
|
}
|
||
|
fallthrough
|
||
|
case reflect.Array:
|
||
|
if val.Len() == 0 {
|
||
|
return cty.ListValEmpty(ety), nil
|
||
|
}
|
||
|
|
||
|
// While we work on our elements we'll temporarily grow
|
||
|
// path to give us a place to put our index step.
|
||
|
path = append(path, cty.PathStep(nil))
|
||
|
|
||
|
vals := make([]cty.Value, val.Len())
|
||
|
for i := range vals {
|
||
|
var err error
|
||
|
path[len(path)-1] = cty.IndexStep{
|
||
|
Key: cty.NumberIntVal(int64(i)),
|
||
|
}
|
||
|
vals[i], err = toCtyValue(val.Index(i), ety, path)
|
||
|
if err != nil {
|
||
|
return cty.NilVal, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Discard our extra path segment, retaining it as extra capacity
|
||
|
// for future appending to the path.
|
||
|
path = path[:len(path)-1]
|
||
|
|
||
|
return cty.ListVal(vals), nil
|
||
|
|
||
|
default:
|
||
|
return cty.NilVal, path.NewErrorf("can't convert Go %s to %#v", val.Kind(), cty.List(ety))
|
||
|
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func toCtyMap(val reflect.Value, ety cty.Type, path cty.Path) (cty.Value, error) {
|
||
|
if val = toCtyUnwrapPointer(val); !val.IsValid() {
|
||
|
return cty.NullVal(cty.Map(ety)), nil
|
||
|
}
|
||
|
|
||
|
switch val.Kind() {
|
||
|
|
||
|
case reflect.Map:
|
||
|
if val.IsNil() {
|
||
|
return cty.NullVal(cty.Map(ety)), nil
|
||
|
}
|
||
|
|
||
|
if val.Len() == 0 {
|
||
|
return cty.MapValEmpty(ety), nil
|
||
|
}
|
||
|
|
||
|
keyType := val.Type().Key()
|
||
|
if keyType.Kind() != reflect.String {
|
||
|
return cty.NilVal, path.NewErrorf("can't convert Go map with key type %s; key type must be string", keyType)
|
||
|
}
|
||
|
|
||
|
// While we work on our elements we'll temporarily grow
|
||
|
// path to give us a place to put our index step.
|
||
|
path = append(path, cty.PathStep(nil))
|
||
|
|
||
|
vals := make(map[string]cty.Value, val.Len())
|
||
|
for _, kv := range val.MapKeys() {
|
||
|
k := kv.String()
|
||
|
var err error
|
||
|
path[len(path)-1] = cty.IndexStep{
|
||
|
Key: cty.StringVal(k),
|
||
|
}
|
||
|
vals[k], err = toCtyValue(val.MapIndex(reflect.ValueOf(k)), ety, path)
|
||
|
if err != nil {
|
||
|
return cty.NilVal, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Discard our extra path segment, retaining it as extra capacity
|
||
|
// for future appending to the path.
|
||
|
path = path[:len(path)-1]
|
||
|
|
||
|
return cty.MapVal(vals), nil
|
||
|
|
||
|
default:
|
||
|
return cty.NilVal, path.NewErrorf("can't convert Go %s to %#v", val.Kind(), cty.Map(ety))
|
||
|
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func toCtySet(val reflect.Value, ety cty.Type, path cty.Path) (cty.Value, error) {
|
||
|
if val = toCtyUnwrapPointer(val); !val.IsValid() {
|
||
|
return cty.NullVal(cty.Set(ety)), nil
|
||
|
}
|
||
|
|
||
|
var vals []cty.Value
|
||
|
|
||
|
switch val.Kind() {
|
||
|
|
||
|
case reflect.Slice:
|
||
|
if val.IsNil() {
|
||
|
return cty.NullVal(cty.Set(ety)), nil
|
||
|
}
|
||
|
fallthrough
|
||
|
case reflect.Array:
|
||
|
if val.Len() == 0 {
|
||
|
return cty.SetValEmpty(ety), nil
|
||
|
}
|
||
|
|
||
|
vals = make([]cty.Value, val.Len())
|
||
|
for i := range vals {
|
||
|
var err error
|
||
|
vals[i], err = toCtyValue(val.Index(i), ety, path)
|
||
|
if err != nil {
|
||
|
return cty.NilVal, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
case reflect.Struct:
|
||
|
|
||
|
if !val.Type().AssignableTo(setType) {
|
||
|
return cty.NilVal, path.NewErrorf("can't convert Go %s to %#v", val.Type(), cty.Set(ety))
|
||
|
}
|
||
|
|
||
|
rawSet := val.Interface().(set.Set)
|
||
|
inVals := rawSet.Values()
|
||
|
|
||
|
if len(inVals) == 0 {
|
||
|
return cty.SetValEmpty(ety), nil
|
||
|
}
|
||
|
|
||
|
vals = make([]cty.Value, len(inVals))
|
||
|
for i := range inVals {
|
||
|
var err error
|
||
|
vals[i], err = toCtyValue(reflect.ValueOf(inVals[i]), ety, path)
|
||
|
if err != nil {
|
||
|
return cty.NilVal, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
default:
|
||
|
return cty.NilVal, path.NewErrorf("can't convert Go %s to %#v", val.Kind(), cty.Set(ety))
|
||
|
|
||
|
}
|
||
|
|
||
|
return cty.SetVal(vals), nil
|
||
|
}
|
||
|
|
||
|
func toCtyObject(val reflect.Value, attrTypes map[string]cty.Type, path cty.Path) (cty.Value, error) {
|
||
|
if val = toCtyUnwrapPointer(val); !val.IsValid() {
|
||
|
return cty.NullVal(cty.Object(attrTypes)), nil
|
||
|
}
|
||
|
|
||
|
switch val.Kind() {
|
||
|
|
||
|
case reflect.Map:
|
||
|
if val.IsNil() {
|
||
|
return cty.NullVal(cty.Object(attrTypes)), nil
|
||
|
}
|
||
|
|
||
|
keyType := val.Type().Key()
|
||
|
if keyType.Kind() != reflect.String {
|
||
|
return cty.NilVal, path.NewErrorf("can't convert Go map with key type %s; key type must be string", keyType)
|
||
|
}
|
||
|
|
||
|
if len(attrTypes) == 0 {
|
||
|
return cty.EmptyObjectVal, nil
|
||
|
}
|
||
|
|
||
|
// While we work on our elements we'll temporarily grow
|
||
|
// path to give us a place to put our GetAttr step.
|
||
|
path = append(path, cty.PathStep(nil))
|
||
|
|
||
|
haveKeys := make(map[string]struct{}, val.Len())
|
||
|
for _, kv := range val.MapKeys() {
|
||
|
haveKeys[kv.String()] = struct{}{}
|
||
|
}
|
||
|
|
||
|
vals := make(map[string]cty.Value, len(attrTypes))
|
||
|
for k, at := range attrTypes {
|
||
|
var err error
|
||
|
path[len(path)-1] = cty.GetAttrStep{
|
||
|
Name: k,
|
||
|
}
|
||
|
|
||
|
if _, have := haveKeys[k]; !have {
|
||
|
vals[k] = cty.NullVal(at)
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
vals[k], err = toCtyValue(val.MapIndex(reflect.ValueOf(k)), at, path)
|
||
|
if err != nil {
|
||
|
return cty.NilVal, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Discard our extra path segment, retaining it as extra capacity
|
||
|
// for future appending to the path.
|
||
|
path = path[:len(path)-1]
|
||
|
|
||
|
return cty.ObjectVal(vals), nil
|
||
|
|
||
|
case reflect.Struct:
|
||
|
if len(attrTypes) == 0 {
|
||
|
return cty.EmptyObjectVal, nil
|
||
|
}
|
||
|
|
||
|
// While we work on our elements we'll temporarily grow
|
||
|
// path to give us a place to put our GetAttr step.
|
||
|
path = append(path, cty.PathStep(nil))
|
||
|
|
||
|
attrFields := structTagIndices(val.Type())
|
||
|
|
||
|
vals := make(map[string]cty.Value, len(attrTypes))
|
||
|
for k, at := range attrTypes {
|
||
|
path[len(path)-1] = cty.GetAttrStep{
|
||
|
Name: k,
|
||
|
}
|
||
|
|
||
|
if fieldIdx, have := attrFields[k]; have {
|
||
|
var err error
|
||
|
vals[k], err = toCtyValue(val.Field(fieldIdx), at, path)
|
||
|
if err != nil {
|
||
|
return cty.NilVal, err
|
||
|
}
|
||
|
} else {
|
||
|
vals[k] = cty.NullVal(at)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Discard our extra path segment, retaining it as extra capacity
|
||
|
// for future appending to the path.
|
||
|
path = path[:len(path)-1]
|
||
|
|
||
|
return cty.ObjectVal(vals), nil
|
||
|
|
||
|
default:
|
||
|
return cty.NilVal, path.NewErrorf("can't convert Go %s to %#v", val.Kind(), cty.Object(attrTypes))
|
||
|
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func toCtyTuple(val reflect.Value, elemTypes []cty.Type, path cty.Path) (cty.Value, error) {
|
||
|
if val = toCtyUnwrapPointer(val); !val.IsValid() {
|
||
|
return cty.NullVal(cty.Tuple(elemTypes)), nil
|
||
|
}
|
||
|
|
||
|
switch val.Kind() {
|
||
|
|
||
|
case reflect.Slice:
|
||
|
if val.IsNil() {
|
||
|
return cty.NullVal(cty.Tuple(elemTypes)), nil
|
||
|
}
|
||
|
|
||
|
if val.Len() != len(elemTypes) {
|
||
|
return cty.NilVal, path.NewErrorf("wrong number of elements %d; need %d", val.Len(), len(elemTypes))
|
||
|
}
|
||
|
|
||
|
if len(elemTypes) == 0 {
|
||
|
return cty.EmptyTupleVal, nil
|
||
|
}
|
||
|
|
||
|
// While we work on our elements we'll temporarily grow
|
||
|
// path to give us a place to put our Index step.
|
||
|
path = append(path, cty.PathStep(nil))
|
||
|
|
||
|
vals := make([]cty.Value, len(elemTypes))
|
||
|
for i, ety := range elemTypes {
|
||
|
var err error
|
||
|
|
||
|
path[len(path)-1] = cty.IndexStep{
|
||
|
Key: cty.NumberIntVal(int64(i)),
|
||
|
}
|
||
|
|
||
|
vals[i], err = toCtyValue(val.Index(i), ety, path)
|
||
|
if err != nil {
|
||
|
return cty.NilVal, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Discard our extra path segment, retaining it as extra capacity
|
||
|
// for future appending to the path.
|
||
|
path = path[:len(path)-1]
|
||
|
|
||
|
return cty.TupleVal(vals), nil
|
||
|
|
||
|
case reflect.Struct:
|
||
|
fieldCount := val.Type().NumField()
|
||
|
if fieldCount != len(elemTypes) {
|
||
|
return cty.NilVal, path.NewErrorf("wrong number of struct fields %d; need %d", fieldCount, len(elemTypes))
|
||
|
}
|
||
|
|
||
|
if len(elemTypes) == 0 {
|
||
|
return cty.EmptyTupleVal, nil
|
||
|
}
|
||
|
|
||
|
// While we work on our elements we'll temporarily grow
|
||
|
// path to give us a place to put our Index step.
|
||
|
path = append(path, cty.PathStep(nil))
|
||
|
|
||
|
vals := make([]cty.Value, len(elemTypes))
|
||
|
for i, ety := range elemTypes {
|
||
|
var err error
|
||
|
|
||
|
path[len(path)-1] = cty.IndexStep{
|
||
|
Key: cty.NumberIntVal(int64(i)),
|
||
|
}
|
||
|
|
||
|
vals[i], err = toCtyValue(val.Field(i), ety, path)
|
||
|
if err != nil {
|
||
|
return cty.NilVal, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Discard our extra path segment, retaining it as extra capacity
|
||
|
// for future appending to the path.
|
||
|
path = path[:len(path)-1]
|
||
|
|
||
|
return cty.TupleVal(vals), nil
|
||
|
|
||
|
default:
|
||
|
return cty.NilVal, path.NewErrorf("can't convert Go %s to %#v", val.Kind(), cty.Tuple(elemTypes))
|
||
|
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func toCtyCapsule(val reflect.Value, capsuleType cty.Type, path cty.Path) (cty.Value, error) {
|
||
|
if val = toCtyUnwrapPointer(val); !val.IsValid() {
|
||
|
return cty.NullVal(capsuleType), nil
|
||
|
}
|
||
|
|
||
|
if val.Kind() != reflect.Ptr {
|
||
|
if !val.CanAddr() {
|
||
|
return cty.NilVal, path.NewErrorf("source value for capsule %#v must be addressable", capsuleType)
|
||
|
}
|
||
|
|
||
|
val = val.Addr()
|
||
|
}
|
||
|
|
||
|
if !val.Type().Elem().AssignableTo(capsuleType.EncapsulatedType()) {
|
||
|
return cty.NilVal, path.NewErrorf("value of type %T not compatible with capsule %#v", val.Interface(), capsuleType)
|
||
|
}
|
||
|
|
||
|
return cty.CapsuleVal(capsuleType, val.Interface()), nil
|
||
|
}
|
||
|
|
||
|
func toCtyDynamic(val reflect.Value, path cty.Path) (cty.Value, error) {
|
||
|
if val = toCtyUnwrapPointer(val); !val.IsValid() {
|
||
|
return cty.NullVal(cty.DynamicPseudoType), nil
|
||
|
}
|
||
|
|
||
|
switch val.Kind() {
|
||
|
|
||
|
case reflect.Struct:
|
||
|
if !val.Type().AssignableTo(valueType) {
|
||
|
return cty.NilVal, path.NewErrorf("can't convert Go %s dynamically; only cty.Value allowed", val.Type())
|
||
|
}
|
||
|
|
||
|
return val.Interface().(cty.Value), nil
|
||
|
|
||
|
default:
|
||
|
return cty.NilVal, path.NewErrorf("can't convert Go %s dynamically; only cty.Value allowed", val.Kind())
|
||
|
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
func toCtyPassthrough(wrappedVal reflect.Value, wantTy cty.Type, path cty.Path) (cty.Value, error) {
|
||
|
if wrappedVal = toCtyUnwrapPointer(wrappedVal); !wrappedVal.IsValid() {
|
||
|
return cty.NullVal(wantTy), nil
|
||
|
}
|
||
|
|
||
|
givenVal := wrappedVal.Interface().(cty.Value)
|
||
|
|
||
|
val, err := convert.Convert(givenVal, wantTy)
|
||
|
if err != nil {
|
||
|
return cty.NilVal, path.NewErrorf("unsuitable value: %s", err)
|
||
|
}
|
||
|
return val, nil
|
||
|
}
|
||
|
|
||
|
// toCtyUnwrapPointer is a helper for dealing with Go pointers. It has three
|
||
|
// possible outcomes:
|
||
|
//
|
||
|
// - Given value isn't a pointer, so it's just returned as-is.
|
||
|
// - Given value is a non-nil pointer, in which case it is dereferenced
|
||
|
// and the result returned.
|
||
|
// - Given value is a nil pointer, in which case an invalid value is returned.
|
||
|
//
|
||
|
// For nested pointer types, like **int, they are all dereferenced in turn
|
||
|
// until a non-pointer value is found, or until a nil pointer is encountered.
|
||
|
func toCtyUnwrapPointer(val reflect.Value) reflect.Value {
|
||
|
for val.Kind() == reflect.Ptr || val.Kind() == reflect.Interface {
|
||
|
if val.IsNil() {
|
||
|
return reflect.Value{}
|
||
|
}
|
||
|
|
||
|
val = val.Elem()
|
||
|
}
|
||
|
|
||
|
return val
|
||
|
}
|