1
1
mirror of https://github.com/mcuadros/ascode synced 2024-11-23 01:11:59 +01:00

provider: type system

This commit is contained in:
Máximo Cuadros 2019-07-03 18:22:36 +02:00
parent aa6b2fdde3
commit 9d2a6fd909
8 changed files with 433 additions and 158 deletions

@ -32,7 +32,7 @@ func (c *ResourceCollection) String() string {
// Type honors the starlark.Value interface.
func (c *ResourceCollection) Type() string {
return fmt.Sprintf("%s_collection", c.typ)
return "collection"
}
// Truth honors the starlark.Value interface.

30
provider/computed.go Normal file

@ -0,0 +1,30 @@
package provider
import (
"fmt"
"github.com/hashicorp/terraform/configs/configschema"
"go.starlark.net/starlark"
)
type sString = starlark.String
type Computed struct {
r *Resource
a *configschema.Attribute
name string
sString
}
func NewComputed(r *Resource, a *configschema.Attribute, name string) *Computed {
return &Computed{
r: r,
a: a,
name: name,
sString: starlark.String(fmt.Sprintf("${%s.%s.%s.%s}", r.kind, r.typ, r.name, name)),
}
}
func (*Computed) Type() string {
return "computed"
}

@ -1,11 +1,8 @@
package provider
import (
"fmt"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hclwrite"
"github.com/zclconf/go-cty/cty"
"go.starlark.net/starlark"
)
@ -51,14 +48,14 @@ func (r *Resource) ToHCL(b *hclwrite.Body) {
}
body := block.Body()
for k, attr := range r.block.Attributes {
for k := range r.block.Attributes {
v, ok := r.values[k]
if !ok {
continue
}
// TODO(mcuadros): I don't know how to do this properly, meanwhile, this works.
if c, ok := v.(*Computed); ok {
if c, ok := v.v.(*Computed); ok {
body.SetAttributeTraversal(k, hcl.Traversal{hcl.TraverseRoot{
Name: c.String(),
}})
@ -66,7 +63,7 @@ func (r *Resource) ToHCL(b *hclwrite.Body) {
continue
}
body.SetAttributeValue(k, EncodeToCty(attr.Type, ValueToNative(v)))
body.SetAttributeValue(k, v.Cty())
}
for k := range r.block.BlockTypes {
@ -75,32 +72,8 @@ func (r *Resource) ToHCL(b *hclwrite.Body) {
continue
}
if collection, ok := v.(HCLCompatible); ok {
if collection, ok := v.Value().(HCLCompatible); ok {
collection.ToHCL(block.Body())
}
}
}
func EncodeToCty(t cty.Type, v interface{}) cty.Value {
switch value := v.(type) {
case string:
return cty.StringVal(value)
case int64:
return cty.NumberIntVal(value)
case bool:
return cty.BoolVal(value)
case []interface{}:
if len(value) == 0 {
return cty.ListValEmpty(t)
}
values := make([]cty.Value, len(value))
for i, v := range value {
values[i] = EncodeToCty(t, v)
}
return cty.ListVal(values)
default:
return cty.StringVal(fmt.Sprintf("unhandled: %T", v))
}
}

@ -5,7 +5,6 @@ import (
"github.com/hashicorp/hcl2/hclwrite"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/zclconf/go-cty/cty"
"go.starlark.net/starlark"
)
@ -24,7 +23,7 @@ type Resource struct {
typ string
kind ResourceKind
block *configschema.Block
values map[string]starlark.Value
values map[string]*Value
}
func MakeResource(name, typ string, k ResourceKind, b *configschema.Block, kwargs []starlark.Tuple) (*Resource, error) {
@ -33,7 +32,7 @@ func MakeResource(name, typ string, k ResourceKind, b *configschema.Block, kwarg
typ: typ,
kind: k,
block: b,
values: make(map[string]starlark.Value),
values: make(map[string]*Value),
}
return r, r.loadKeywordArgs(kwargs)
@ -69,7 +68,7 @@ func (r *Resource) String() string {
// Type honors the starlark.Value interface.
func (r *Resource) Type() string {
return r.typ
return "resource"
}
// Truth honors the starlark.Value interface.
@ -87,7 +86,7 @@ func (r *Resource) Hash() (uint32, error) {
for name, value := range r.values {
namehash, _ := starlark.String(name).Hash()
x = x ^ 3*namehash
y, err := value.Hash()
y, err := value.Value().Hash()
if err != nil {
return 0, err
}
@ -116,7 +115,7 @@ func (r *Resource) Attr(name string) (starlark.Value, error) {
}
if v, ok := r.values[name]; ok {
return v, nil
return v.Value(), nil
}
return nil, nil
@ -128,15 +127,16 @@ func (r *Resource) attrComputed(name string, attr *configschema.Attribute) (star
func (r *Resource) attrBlock(name string, b *configschema.NestedBlock) (starlark.Value, error) {
if b.MaxItems != 1 {
if _, ok := r.values[name]; !ok {
r.values[name] = NewResourceCollection(name, NestedK, &b.Block)
r.values[name] = MustValue(NewResourceCollection(name, NestedK, &b.Block))
}
}
if _, ok := r.values[name]; !ok {
r.values[name], _ = MakeResource("", name, NestedK, &b.Block, nil)
resource, _ := MakeResource("", name, NestedK, &b.Block, nil)
r.values[name] = MustValue(resource)
}
return r.values[name], nil
return r.values[name].Value(), nil
}
// AttrNames honors the starlark.HasAttrs interface.
@ -169,11 +169,11 @@ func (r *Resource) SetField(name string, v starlark.Value) error {
return starlark.NoSuchAttrError(errmsg)
}
if err := ValidateType(v, attr.Type); err != nil {
if err := MustTypeFromCty(attr.Type).Validate(v); err != nil {
return err
}
r.values[name] = v
r.values[name] = MustValue(v)
return nil
}
@ -190,127 +190,18 @@ func (r *Resource) setFieldFromNestedBlock(name string, b *configschema.NestedBl
func (r *Resource) toDict() *starlark.Dict {
d := starlark.NewDict(len(r.values))
for k, v := range r.values {
if r, ok := v.(*Resource); ok {
if r, ok := v.Value().(*Resource); ok {
d.SetKey(starlark.String(k), r.toDict())
continue
}
if r, ok := v.(*ResourceCollection); ok {
if r, ok := v.Value().(*ResourceCollection); ok {
d.SetKey(starlark.String(k), r.toDict())
continue
}
d.SetKey(starlark.String(k), v)
d.SetKey(starlark.String(k), v.Value())
}
return d
}
func ValueToNative(v starlark.Value) interface{} {
switch cast := v.(type) {
case starlark.Bool:
return bool(cast)
case starlark.String:
return string(cast)
case starlark.Int:
i, _ := cast.Int64()
return i
case *ResourceCollection:
return ValueToNative(cast.List)
case *starlark.List:
out := make([]interface{}, cast.Len())
for i := 0; i < cast.Len(); i++ {
out[i] = ValueToNative(cast.Index(i))
}
return out
default:
return v
}
}
/*
NoneType # the type of None
bool # True or False
int # a signed integer of arbitrary magnitude
float # an IEEE 754 double-precision floating point number
string # a byte string
list # a modifiable sequence of values
tuple # an unmodifiable sequence of values
dict # a mapping from values to values
set # a set of values
function # a function implemented in Starlark
builtin_function_or_method
*/
func FromStarlarkType(typ string) cty.Type {
switch typ {
case "bool":
return cty.Bool
case "int":
case "float":
return cty.Number
case "string":
return cty.String
}
return cty.NilType
}
func ValidateListType(l *starlark.List, expected cty.Type) error {
for i := 0; i < l.Len(); i++ {
if err := ValidateType(l.Index(i), expected); err != nil {
return fmt.Errorf("index %d: %s", i, err)
}
}
return nil
}
func ValidateType(v starlark.Value, expected cty.Type) error {
switch v.(type) {
case starlark.String:
if expected == cty.String {
return nil
}
case starlark.Int:
if expected == cty.Number {
return nil
}
case starlark.Bool:
if expected == cty.Bool {
return nil
}
case *Computed:
if expected == v.(*Computed).a.Type {
return nil
}
case *starlark.List:
if expected.IsListType() || expected.IsSetType() {
return ValidateListType(v.(*starlark.List), expected.ElementType())
}
}
return fmt.Errorf("expected %s, got %s", ToStarlarkType(expected), v.Type())
}
func ToStarlarkType(t cty.Type) string {
switch t {
case cty.String:
return "string"
case cty.Number:
return "int"
case cty.Bool:
return "bool"
}
if t.IsListType() {
return "list"
}
if t.IsSetType() {
return "set"
}
return "(unknown)"
}

@ -3,11 +3,11 @@ load("assert.star", "assert")
p = provider("aws", "2.13.0")
d = p.data.ami("foo")
assert.eq(type(d.filter), "filter_collection")
assert.eq(type(d.filter), "collection")
bar = d.filter(name="bar", values=["qux"])
assert.eq(type(bar), "filter")
assert.eq(type(bar), "resource")
assert.eq(bar.name, "bar")
assert.eq(bar.values, ["qux"])

@ -8,7 +8,7 @@ assert.eq(len(dir(p.resource)), 506)
resources = dir(p.resource)
assert.contains(resources, "instance")
assert.eq(type(p.resource.instance), "aws_instance_collection")
assert.eq(type(p.resource.instance), "collection")
p.resource.instance("foo")
p.resource.instance("bar")

230
provider/type.go Normal file

@ -0,0 +1,230 @@
package provider
import (
"fmt"
"github.com/zclconf/go-cty/cty"
"go.starlark.net/starlark"
)
// Value is helper to manipulate and transform starlark.Value to go types and
// cty.Value.
type Value struct {
t Type
v starlark.Value
}
// MustValue returns a Value from a starlark.Value, it panics if error.
func MustValue(v starlark.Value) *Value {
value, err := NewValue(v)
if err != nil {
panic(err)
}
return value
}
// NewValue returns a Value from a starlark.Value.
func NewValue(v starlark.Value) (*Value, error) {
t, err := NewTypeFromStarlark(v.Type())
if err != nil {
return nil, err
}
return &Value{t: *t, v: v}, nil
}
// Value returns the starlark.Value.
func (v *Value) Value() starlark.Value {
return v.v
}
// Type returns the Type of the value.
func (v *Value) Type() *Type {
return &v.t
}
// Cty returns the cty.Value.
func (v *Value) Cty() cty.Value {
switch v.t.Starlark() {
case "string":
return cty.StringVal(v.Interface().(string))
case "int":
return cty.NumberIntVal(v.Interface().(int64))
case "float":
return cty.NumberFloatVal(v.Interface().(float64))
case "bool":
return cty.BoolVal(v.Interface().(bool))
case "list":
list := v.v.(*starlark.List)
if list.Len() == 0 {
return cty.ListValEmpty(v.t.Cty())
}
values := make([]cty.Value, list.Len())
for i := 0; i < list.Len(); i++ {
values[i] = MustValue(list.Index(i)).Cty()
}
return cty.ListVal(values)
default:
return cty.StringVal(fmt.Sprintf("unhandled: %s", v.t.typ))
}
}
// Interface returns the value as a Go value.
func (v *Value) Interface() interface{} {
switch cast := v.v.(type) {
case starlark.Bool:
return bool(cast)
case starlark.String:
return string(cast)
case starlark.Int:
i, _ := cast.Int64()
return i
case starlark.Float:
return float64(cast)
case *ResourceCollection:
return MustValue(cast.List).Interface()
case *starlark.List:
out := make([]interface{}, cast.Len())
for i := 0; i < cast.Len(); i++ {
out[i] = MustValue(cast.Index(i)).Interface()
}
return out
default:
return v
}
}
// Type is a helper to manipulate and transform starlark.Type and cty.Type
type Type struct {
typ string
cty cty.Type
}
// MustTypeFromStarlark returns a Type from a given starlark type string.
// Panics if error.
func MustTypeFromStarlark(typ string) *Type {
t, err := NewTypeFromStarlark(typ)
if err != nil {
panic(err)
}
return t
}
// NewTypeFromStarlark returns a Type from a given starlark type string.
func NewTypeFromStarlark(typ string) (*Type, error) {
t := &Type{}
t.typ = typ
switch typ {
case "bool":
t.cty = cty.Bool
case "int", "float":
t.cty = cty.Number
case "string":
t.cty = cty.String
case "collection":
t.cty = cty.List(cty.NilType)
case "resource":
t.cty = cty.Map(cty.NilType)
case "list":
t.cty = cty.List(cty.NilType)
case "computed":
t.cty = cty.String
default:
return nil, fmt.Errorf("unexpected %q type", typ)
}
return t, nil
}
// MustTypeFromCty returns a Type froma given cty.Type. Panics if error.
func MustTypeFromCty(typ cty.Type) *Type {
t, err := NewTypeFromCty(typ)
if err != nil {
panic(err)
}
return t
}
// NewTypeFromCty returns a Type froma given cty.Type.
func NewTypeFromCty(typ cty.Type) (*Type, error) {
t := &Type{}
t.cty = typ
switch typ {
case cty.String:
t.typ = "string"
case cty.Number:
t.typ = "int"
case cty.Bool:
t.typ = "bool"
}
if typ.IsListType() {
t.typ = "list"
}
if typ.IsSetType() {
t.typ = "set"
}
if typ.IsTupleType() {
t.typ = "tuple"
}
return t, nil
}
// Starlark returns the type as starlark type string.
func (t *Type) Starlark() string {
return t.typ
}
// Cty returns the type as cty.Type.
func (t *Type) Cty() cty.Type {
return t.cty
}
// Validate validates a value againts the type.
func (t *Type) Validate(v starlark.Value) error {
switch v.(type) {
case starlark.String:
if t.cty == cty.String {
return nil
}
case starlark.Int, starlark.Float:
if t.cty == cty.Number {
return nil
}
case starlark.Bool:
if t.cty == cty.Bool {
return nil
}
case *Computed:
if t.cty == v.(*Computed).a.Type {
return nil
}
case *starlark.List:
if t.cty.IsListType() || t.cty.IsSetType() {
return t.validateListType(v.(*starlark.List), t.cty.ElementType())
}
}
return fmt.Errorf("expected %s, got %s", t.typ, v.Type())
}
func (t *Type) validateListType(l *starlark.List, expected cty.Type) error {
for i := 0; i < l.Len(); i++ {
if err := MustTypeFromCty(expected).Validate(l.Index(i)); err != nil {
return fmt.Errorf("index %d: %s", i, err)
}
}
return nil
}

151
provider/type_test.go Normal file

@ -0,0 +1,151 @@
package provider
import (
"testing"
"go.starlark.net/starlark"
"github.com/stretchr/testify/assert"
"github.com/zclconf/go-cty/cty"
)
func TestNewTypeFromStarlark(t *testing.T) {
testCases := []struct {
typ string
cty cty.Type
}{
{"bool", cty.Bool},
{"int", cty.Number},
{"float", cty.Number},
{"string", cty.String},
}
for _, tc := range testCases {
typ, err := NewTypeFromStarlark(tc.typ)
assert.NoError(t, err)
assert.Equal(t, typ.Cty(), tc.cty)
}
}
func TestNewTypeFromStarlark_NonScalar(t *testing.T) {
typ := MustTypeFromStarlark("list")
assert.True(t, typ.Cty().IsListType())
typ = MustTypeFromStarlark("collection")
assert.True(t, typ.Cty().IsListType())
typ = MustTypeFromStarlark("resource")
assert.True(t, typ.Cty().IsMapType())
}
func TestNewTypeFromCty(t *testing.T) {
testCases := []struct {
typ string
cty cty.Type
}{
{"string", cty.String},
{"int", cty.Number},
{"bool", cty.Bool},
{"list", cty.List(cty.String)},
{"set", cty.Set(cty.String)},
{"tuple", cty.Tuple([]cty.Type{})},
}
for _, tc := range testCases {
typ, err := NewTypeFromCty(tc.cty)
assert.NoError(t, err)
assert.Equal(t, typ.Starlark(), tc.typ)
}
}
func TestTypeValidate(t *testing.T) {
testCases := []struct {
t string
v starlark.Value
err bool
}{
{"string", starlark.String("foo"), false},
{"int", starlark.String("foo"), true},
{"int", starlark.MakeInt(42), false},
{"int", starlark.MakeInt64(42), false},
{"string", starlark.MakeInt(42), true},
{"int", starlark.Float(42.), false},
}
for _, tc := range testCases {
typ := MustTypeFromStarlark(tc.t)
err := typ.Validate(tc.v)
if tc.err {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
}
}
func TestTypeValidate_List(t *testing.T) {
typ := MustTypeFromCty(cty.List(cty.String))
err := typ.Validate(starlark.NewList([]starlark.Value{
starlark.String("foo"),
starlark.String("bar"),
}))
assert.NoError(t, err)
}
func TestTypeValidate_ListError(t *testing.T) {
typ := MustTypeFromCty(cty.List(cty.Number))
err := typ.Validate(starlark.NewList([]starlark.Value{
starlark.MakeInt(42),
starlark.String("bar"),
}))
assert.Errorf(t, err, "index 1: expected int, got string")
}
func TestMustValue(t *testing.T) {
testCases := []struct {
v starlark.Value
cty cty.Type
value cty.Value
native interface{}
}{
{
starlark.String("foo"),
cty.String,
cty.StringVal("foo"),
"foo",
},
{
starlark.MakeInt(42),
cty.Number,
cty.NumberIntVal(42),
int64(42),
},
{
starlark.Float(42),
cty.Number,
cty.NumberFloatVal(42),
42.,
},
{
starlark.Bool(true),
cty.Bool,
cty.True,
true,
},
{
starlark.NewList([]starlark.Value{starlark.String("foo")}),
cty.List(cty.NilType),
cty.ListVal([]cty.Value{cty.StringVal("foo")}),
[]interface{}{"foo"},
},
}
for _, tc := range testCases {
value := MustValue(tc.v)
assert.Equal(t, value.Type().Cty(), tc.cty)
assert.Equal(t, value.Value(), tc.v)
assert.Equal(t, value.Cty(), tc.value)
assert.Equal(t, value.Interface(), tc.native)
}
}