diff --git a/attr/type.go b/attr/type.go index b82a0d30..252b3891 100644 --- a/attr/type.go +++ b/attr/type.go @@ -26,6 +26,8 @@ type Type interface { // Equal must return true if the Type is considered semantically equal // to the Type passed as an argument. Equal(Type) bool + + tftypes.AttributePathStepper } // TypeWithAttributeTypes extends the Type interface to include information about diff --git a/schema/attribute.go b/schema/attribute.go index c640a7ce..faf0f6ca 100644 --- a/schema/attribute.go +++ b/schema/attribute.go @@ -1,6 +1,11 @@ package schema -import "github.com/hashicorp/terraform-plugin-framework/attr" +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) // Attribute defines the constraints and behaviors of a single field in a // schema. Attributes are the fields that show up in Terraform state files and @@ -60,3 +65,14 @@ type Attribute struct { // instructing them on what upgrade steps to take. DeprecationMessage string } + +func (a Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + if a.Type != nil { + return a.Type.ApplyTerraform5AttributePathStep(step) + } + if a.Attributes != nil { + return a.Attributes.ApplyTerraform5AttributePathStep(step) + } + + return nil, fmt.Errorf("could not apply step %T to Attribute, because it has no Type or Attributes set", step) +} diff --git a/schema/nested_attributes.go b/schema/nested_attributes.go index 8b995457..ffa93603 100644 --- a/schema/nested_attributes.go +++ b/schema/nested_attributes.go @@ -1,5 +1,11 @@ package schema +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + type nestingMode uint8 const ( @@ -34,6 +40,7 @@ const ( type NestedAttributes interface { getNestingMode() nestingMode getAttributes() map[string]Attribute + tftypes.AttributePathStepper } type nestedAttributes map[string]Attribute @@ -59,6 +66,14 @@ func (s singleNestedAttributes) getNestingMode() nestingMode { return nestingModeSingle } +func (s singleNestedAttributes) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + if _, ok := step.(tftypes.ElementKeyString); !ok { + return nil, fmt.Errorf("cannot apply step %T to SingleNestedAttributes", step) + } + + return s.nestedAttributes, nil +} + // ListNestedAttributes nests `attributes` under another attribute, allowing // multiple instances of that group of attributes to appear in the // configuration. Minimum and maximum numbers of times the group can appear in @@ -88,6 +103,14 @@ func (l listNestedAttributes) getNestingMode() nestingMode { return nestingModeList } +func (l listNestedAttributes) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + if _, ok := step.(tftypes.ElementKeyInt); !ok { + return nil, fmt.Errorf("cannot apply step %T to ListNestedAttributes", step) + } + + return l.nestedAttributes, nil +} + // SetNestedAttributes nests `attributes` under another attribute, allowing // multiple instances of that group of attributes to appear in the // configuration, while requiring each group of values be unique. Minimum and @@ -118,6 +141,14 @@ func (s setNestedAttributes) getNestingMode() nestingMode { return nestingModeSet } +func (s setNestedAttributes) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + if _, ok := step.(tftypes.ElementKeyInt); !ok { + return nil, fmt.Errorf("cannot apply step %T to ListNestedAttributes", step) + } + + return s.nestedAttributes, nil +} + // MapNestedAttributes nests `attributes` under another attribute, allowing // multiple instances of that group of attributes to appear in the // configuration. Each group will need to be associated with a unique string by @@ -147,3 +178,11 @@ type MapNestedAttributesOptions struct { func (m mapNestedAttributes) getNestingMode() nestingMode { return nestingModeMap } + +func (m mapNestedAttributes) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + if _, ok := step.(tftypes.ElementKeyString); !ok { + return nil, fmt.Errorf("cannot apply step %T to SingleNestedAttributes", step) + } + + return m.nestedAttributes, nil +} diff --git a/schema/schema.go b/schema/schema.go index e8a62e74..5cf6b8a5 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -1,5 +1,14 @@ package schema +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + // Schema is used to define the shape of practitioner-provider information, // like resources, data sources, and providers. Think of it as a type // definition, but for Terraform. @@ -17,3 +26,41 @@ type Schema struct { // Versions should only be incremented by one each release. Version int64 } + +func (s Schema) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + if v, ok := step.(tftypes.AttributeName); ok { + if attr, ok := s.Attributes[string(v)]; ok { + return attr, nil + } else { + return nil, fmt.Errorf("could not find attribute %q in schema", v) + } + } else { + return nil, fmt.Errorf("cannot apply AttributePathStep %T to schema", step) + } +} + +func (s Schema) AttributeType() attr.Type { + attrTypes := map[string]attr.Type{} + for name, attr := range s.Attributes { + if attr.Type != nil { + attrTypes[name] = attr.Type + } + if attr.Attributes != nil { + // TODO: handle nested attributes + } + } + return types.ObjectType{AttrTypes: attrTypes} +} + +func (s Schema) TerraformType(ctx context.Context) tftypes.Type { + attrTypes := map[string]tftypes.Type{} + for name, attr := range s.Attributes { + if attr.Type != nil { + attrTypes[name] = attr.Type.TerraformType(ctx) + } + if attr.Attributes != nil { + // TODO: handle nested attributes + } + } + return tftypes.Object{AttributeTypes: attrTypes} +} diff --git a/state.go b/state.go new file mode 100644 index 00000000..9db2d514 --- /dev/null +++ b/state.go @@ -0,0 +1,41 @@ +package tfsdk + +import ( + "context" + "reflect" + "regexp" + + "github.com/hashicorp/terraform-plugin-framework/attr" + tfReflect "github.com/hashicorp/terraform-plugin-framework/internal/reflect" + "github.com/hashicorp/terraform-plugin-framework/schema" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +var attributeValueReflectType = reflect.TypeOf(new(attr.Value)).Elem() + +type State struct { + Raw tftypes.Value + Schema schema.Schema +} + +func isValidFieldName(name string) bool { + re := regexp.MustCompile("^[a-z][a-z0-9_]*$") + return re.MatchString(name) +} + +// Get populates the struct passed as `target` with the entire state. No type assertion necessary. +func (s State) Get(ctx context.Context, target interface{}) error { + return tfReflect.Into(ctx, s.Schema.AttributeType(), s.Raw, target, tfReflect.Options{}) +} + +// // GetAttribute retrieves the attribute found at `path` and returns it as an attr.Value, +// // which provider developers need to assert the type of +// func (s State) GetAttribute(ctx context.Context, path tftypes.AttributePath) (attr.Value, error) { + +// } + +// // MustGetAttribute retrieves the attribute as GetAttribute does, but populates target using As, +// // using the simplified representation without Unknown. Errors if Unknown present +// func (s State) MustGetAttribute(ctx context.Context, path tftypes.AttributePath, target interface{}) error { +// return nil +// } diff --git a/state_test.go b/state_test.go new file mode 100644 index 00000000..a96a7b7f --- /dev/null +++ b/state_test.go @@ -0,0 +1,81 @@ +package tfsdk + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestStateGet(t *testing.T) { + schema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "foo": { + Type: types.StringType, + Required: true, + }, + "bar": { + Type: types.ListType{ + ElemType: types.StringType, + }, + Required: true, + }, + }, + } + state := State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.List{ElementType: tftypes.String}, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.String, "hello, world"), + "bar": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "red"), + tftypes.NewValue(tftypes.String, "blue"), + tftypes.NewValue(tftypes.String, "green"), + }), + }), + Schema: schema, + } + type myType struct { + Foo types.String `tfsdk:"foo"` + Bar types.List `tfsdk:"bar"` + } + var val myType + err := state.Get(context.Background(), &val) + if err != nil { + t.Errorf("Error running As: %s", err) + } + if val.Foo.Unknown { + t.Error("Expected Foo to be known") + } + if val.Foo.Null { + t.Error("Expected Foo to be non-null") + } + if val.Foo.Value != "hello, world" { + t.Errorf("Expected Foo to be %q, got %q", "hello, world", val.Foo.Value) + } + if val.Bar.Unknown { + t.Error("Expected Bar to be known") + } + if val.Bar.Null { + t.Errorf("Expected Bar to be non-null") + } + if len(val.Bar.Elems) != 3 { + t.Errorf("Expected Bar to have 3 elements, had %d", len(val.Bar.Elems)) + } + if val.Bar.Elems[0].(types.String).Value != "red" { + t.Errorf("Expected Bar's first element to be %q, got %q", "red", val.Bar.Elems[0].(types.String).Value) + } + if val.Bar.Elems[1].(types.String).Value != "blue" { + t.Errorf("Expected Bar's second element to be %q, got %q", "blue", val.Bar.Elems[1].(types.String).Value) + } + if val.Bar.Elems[2].(types.String).Value != "green" { + t.Errorf("Expected Bar's third element to be %q, got %q", "green", val.Bar.Elems[2].(types.String).Value) + } +} diff --git a/types/list.go b/types/list.go index 0766bf96..a8f842c9 100644 --- a/types/list.go +++ b/types/list.go @@ -91,6 +91,14 @@ func (l ListType) Equal(o attr.Type) bool { return l.ElemType.Equal(other.ElemType) } +func (l ListType) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + if _, ok := step.(tftypes.ElementKeyInt); !ok { + return nil, fmt.Errorf("cannot apply step %T to ListType", step) + } + + return l.ElemType, nil +} + // List represents a list of AttributeValues, all of the same type, indicated // by ElemType. type List struct { diff --git a/types/object.go b/types/object.go index eaba7a7a..cf698b3d 100644 --- a/types/object.go +++ b/types/object.go @@ -101,6 +101,14 @@ func (o ObjectType) Equal(candidate attr.Type) bool { return true } +func (o ObjectType) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + if _, ok := step.(tftypes.ElementKeyString); !ok { + return nil, fmt.Errorf("cannot apply step %T to ObjectType", step) + } + + return o.AttrTypes[string(step.(tftypes.ElementKeyString))], nil +} + // Object represents an object type Object struct { // Unknown will be set to true if the entire object is an unknown value. diff --git a/types/primitive.go b/types/primitive.go index f696d0e0..39aeed44 100644 --- a/types/primitive.go +++ b/types/primitive.go @@ -88,3 +88,7 @@ func (p primitive) Equal(o attr.Type) bool { return false } } + +func (p primitive) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return nil, fmt.Errorf("cannot apply AttributePathStep %T to %s", step, p.String()) +} diff --git a/types/primitive_test.go b/types/primitive_test.go index cccfaa60..9a71d968 100644 --- a/types/primitive_test.go +++ b/types/primitive_test.go @@ -67,6 +67,10 @@ func (t testAttributeType) Equal(_ attr.Type) bool { panic("not implemented") } +func (t testAttributeType) ApplyTerraform5AttributePathStep(_ tftypes.AttributePathStep) (interface{}, error) { + panic("not implemented") +} + func TestPrimitiveEqual(t *testing.T) { t.Parallel()