-
Notifications
You must be signed in to change notification settings - Fork 94
Commit
Spitball on what retrieving state in a way that lets users access it with types and no type assertions may look like.
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package attr | ||
This comment has been minimized.
Sorry, something went wrong. |
||
|
||
import ( | ||
"context" | ||
|
||
"github.com/hashicorp/terraform-plugin-go/tfprotov6" | ||
"github.com/hashicorp/terraform-plugin-go/tftypes" | ||
) | ||
|
||
// Type defines an interface for describing a kind of attribute. Types are | ||
// collections of constraints and behaviors such that they can be reused on | ||
// multiple attributes easily. | ||
type Type interface { | ||
// TerraformType returns the tftypes.Type that should be used to | ||
// represent this type. This constrains what user input will be | ||
// accepted and what kind of data can be set in state. The framework | ||
// will use this to translate the Type to something Terraform | ||
// can understand. | ||
TerraformType(context.Context) tftypes.Type | ||
|
||
// Validate returns any warnings or errors about the value that is | ||
// being used to populate the Type. It is generally used to | ||
// check the data format and ensure that it complies with the | ||
// requirements of the Type. | ||
// | ||
// TODO: don't use tfprotov6.Diagnostic, use our type | ||
Validate(context.Context, tftypes.Value) []*tfprotov6.Diagnostic | ||
|
||
// Description returns a practitioner-friendly explanation of the type | ||
// and the constraints of the data it accepts and returns. It will be | ||
// combined with the Description associated with the Attribute. | ||
Description(context.Context) string | ||
|
||
// ValueFromTerraform returns an Value given a tftypes.Value. This is | ||
// meant to convert the tftypes.Value into a more convenient Go type | ||
// for the provider to consume the data with. | ||
ValueFromTerraform(context.Context, tftypes.Value) (Value, error) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package attr | ||
|
||
import "context" | ||
|
||
// Value defines an interface for describing data associated with an attribute. | ||
// Values allow provider developers to specify data in a convenient format, and | ||
// have it transparently be converted to formats Terraform understands. | ||
type Value interface { | ||
// ToTerraformValue returns the data contained in the Value as a Go | ||
// type that tftypes.NewValue will accept. | ||
ToTerraformValue(context.Context) (interface{}, error) | ||
|
||
// Equal must return true if the Value is considered semantically equal | ||
// to the Value passed as an argument. | ||
Equal(Value) bool | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
package tf | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"reflect" | ||
"regexp" | ||
"strings" | ||
|
||
"github.com/hashicorp/terraform-plugin-framework/attr" | ||
"github.com/hashicorp/terraform-plugin-framework/types" | ||
"github.com/hashicorp/terraform-plugin-go/tftypes" | ||
) | ||
|
||
var attributeValueReflectType = reflect.TypeOf(new(attr.Value)).Elem() | ||
|
||
type MyType struct { | ||
Foo types.String `tfsdk:"foo"` | ||
Bar types.List `tfsdk:"bar"` | ||
} | ||
This comment has been minimized.
Sorry, something went wrong. |
||
|
||
type State struct { | ||
Raw tftypes.Value | ||
Schema Schema | ||
} | ||
|
||
func isValidFieldName(name string) bool { | ||
re := regexp.MustCompile("^[a-z][a-z0-9_]*$") | ||
return re.MatchString(name) | ||
} | ||
|
||
func (s State) As(ctx context.Context, in interface{}) error { | ||
reflectValue := reflect.ValueOf(in) | ||
reflectType := reflect.TypeOf(in) | ||
reflectKind := reflectType.Kind() | ||
for reflectKind == reflect.Interface || reflectKind == reflect.Ptr { | ||
reflectValue = reflectValue.Elem() | ||
reflectType = reflectValue.Type() | ||
reflectKind = reflectType.Kind() | ||
} | ||
|
||
if reflectKind != reflect.Struct { | ||
return fmt.Errorf("can only pass structs to As, can't use %s", reflectKind) | ||
} | ||
|
||
structFields := map[string]int{} | ||
for i := 0; i < reflectType.NumField(); i++ { | ||
field := reflectType.Field(i) | ||
if field.PkgPath != "" { | ||
// skip unexported fields | ||
continue | ||
} | ||
tag := field.Tag.Get(`tfsdk`) | ||
if tag == "-" { | ||
// skip explicitly skipped fields | ||
continue | ||
} | ||
if tag == "" { | ||
return fmt.Errorf("Need a tfsdk tag on %s to use As", field.Name) | ||
} | ||
if !isValidFieldName(tag) { | ||
return fmt.Errorf("Can't use %q as a field name, must only contain a-z (lowercase), underscores, and numbers, and must start with a letter.", tag) | ||
} | ||
if other, ok := structFields[tag]; ok { | ||
return fmt.Errorf("Can't use %s as a field name for both %s and %s", tag, reflectType.Field(other).Name, field.Name) | ||
} | ||
structFields[tag] = i | ||
} | ||
|
||
var raw map[string]tftypes.Value | ||
err := s.Raw.As(&raw) | ||
if err != nil { | ||
return fmt.Errorf("error asserting type of state: %w", err) | ||
} | ||
|
||
var stateMissing []string | ||
var structMissing []string | ||
for k := range structFields { | ||
if _, ok := raw[k]; !ok { | ||
stateMissing = append(stateMissing, k) | ||
} | ||
} | ||
for k := range raw { | ||
if _, ok := raw[k]; !ok { | ||
structMissing = append(structMissing, k) | ||
} | ||
} | ||
if len(stateMissing) > 0 || len(structMissing) > 0 { | ||
var missing []string | ||
if len(stateMissing) > 0 { | ||
var fields string | ||
if len(stateMissing) == 1 { | ||
fields = stateMissing[0] | ||
} else if len(stateMissing) == 2 { | ||
fields = strings.Join(stateMissing, " and ") | ||
} else { | ||
stateMissing[len(stateMissing)-1] = "and " + stateMissing[len(stateMissing)-1] | ||
fields = strings.Join(stateMissing, ", ") | ||
} | ||
missing = append(missing, fmt.Sprintf("Struct defines fields (%s) that weren't included in the request.", fields)) | ||
} | ||
if len(structMissing) > 0 { | ||
var fields string | ||
if len(structMissing) == 1 { | ||
fields = structMissing[0] | ||
} else if len(structMissing) == 2 { | ||
fields = strings.Join(structMissing, " and ") | ||
} else { | ||
structMissing[len(structMissing)-1] = "and " + structMissing[len(structMissing)-1] | ||
fields = strings.Join(structMissing, ", ") | ||
} | ||
missing = append(missing, fmt.Sprintf("Struct defines fields (%s) that weren't included in the request.", fields)) | ||
} | ||
return fmt.Errorf("Invalid struct definition for this request: " + strings.Join(missing, " ")) | ||
} | ||
for tag, i := range structFields { | ||
field := reflectType.Field(i) | ||
if !field.Type.Implements(attributeValueReflectType) { | ||
return fmt.Errorf("%s doesn't fill the attr.Value interface", field.Name) | ||
} | ||
fieldValue := reflectValue.Field(i) | ||
if !fieldValue.CanSet() { | ||
return fmt.Errorf("can't set %s", field.Name) | ||
} | ||
|
||
// find out how to instantiate new value of that type | ||
// pull the attr.Type out of the schema | ||
schemaAttr, ok := s.Schema.Attributes[tag] | ||
if !ok { | ||
return fmt.Errorf("Couldn't find a schema for %s", tag) | ||
} | ||
attrValue, err := schemaAttr.Type.ValueFromTerraform(ctx, raw[tag]) | ||
if err != nil { | ||
return fmt.Errorf("Error converting %q from state: %w", tag, err) | ||
} | ||
|
||
newValue := reflect.ValueOf(attrValue) | ||
if !newValue.Type().AssignableTo(field.Type) { | ||
return fmt.Errorf("can't assign %s to %s for %s", newValue.Type().Name(), field.Type.Name(), field.Name) | ||
} | ||
|
||
fieldValue.Set(newValue) | ||
} | ||
return nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
package tf | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/hashicorp/terraform-plugin-framework/types" | ||
"github.com/hashicorp/terraform-plugin-go/tftypes" | ||
) | ||
|
||
func TestStateAs(t *testing.T) { | ||
schema := Schema{ | ||
Attributes: map[string]Attribute{ | ||
"foo": { | ||
Type: types.StringType{}, | ||
Required: true, | ||
}, | ||
"bar": { | ||
Type: types.ListType{ | ||
ElemType: types.StringType{}, | ||
}, | ||
Required: true, | ||
}, | ||
}, | ||
} | ||
This comment has been minimized.
Sorry, something went wrong.
paddycarver
Author
Contributor
|
||
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, | ||
} | ||
This comment has been minimized.
Sorry, something went wrong.
paddycarver
Author
Contributor
|
||
type myType struct { | ||
Foo types.String `tfsdk:"foo"` | ||
Bar types.List `tfsdk:"bar"` | ||
} | ||
var val myType | ||
err := state.As(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) | ||
This comment has been minimized.
Sorry, something went wrong.
paddycarver
Author
Contributor
|
||
} | ||
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) | ||
} | ||
} |
I remain unhappy about this package division. I'll need to think more about some package architecture stuff to see what kind of design makes sense to me.