From 7099e68e4a6d060f6b343dfd3fb42f68a85e721d Mon Sep 17 00:00:00 2001 From: Nicholas Wiersma Date: Wed, 18 Oct 2023 12:56:16 +0200 Subject: [PATCH] feat: use tfconv module (#8) --- ec/armada/converter.go | 6 +- go.mod | 9 +- go.sum | 2 + internal/cmd/schema-gen/gen.go | 2 +- internal/terra/expand.go | 158 -------------- internal/terra/expand_test.go | 73 ------- internal/terra/flatten.go | 190 ----------------- internal/terra/flatten_test.go | 68 ------ internal/terra/schemagen/generator.go | 232 --------------------- internal/terra/schemagen/generator_test.go | 104 --------- internal/terra/terra.go | 52 ----- internal/terra/terra_test.go | 107 ---------- 12 files changed, 10 insertions(+), 993 deletions(-) delete mode 100644 internal/terra/expand.go delete mode 100644 internal/terra/expand_test.go delete mode 100644 internal/terra/flatten.go delete mode 100644 internal/terra/flatten_test.go delete mode 100644 internal/terra/schemagen/generator.go delete mode 100644 internal/terra/schemagen/generator_test.go delete mode 100644 internal/terra/terra.go delete mode 100644 internal/terra/terra_test.go diff --git a/ec/armada/converter.go b/ec/armada/converter.go index 41321f2..05f9258 100644 --- a/ec/armada/converter.go +++ b/ec/armada/converter.go @@ -1,12 +1,12 @@ package armada import ( - "github.com/nitrado/terraform-provider-ec/internal/terra" + "github.com/nitrado/tfconv" "k8s.io/apimachinery/pkg/api/resource" ) -func converter() *terra.Converter { - c := terra.New() +func converter() *tfconv.Converter { + c := tfconv.New("json") c.Register(resource.Quantity{}, expandQuantity, flattenQuantity) return c } diff --git a/go.mod b/go.mod index ede30ab..90f6284 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,14 @@ module github.com/nitrado/terraform-provider-ec -go 1.21.1 - -toolchain go1.21.3 +go 1.21.3 require ( - github.com/ettle/strcase v0.1.1 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/terraform-plugin-docs v0.16.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.29.0 + github.com/nitrado/tfconv v1.0.0 github.com/stretchr/testify v1.8.4 gitlab.com/nitrado/b2b/ec/armada v0.1.8 - golang.org/x/exp v0.0.0-20231006140011-7918f672742d golang.org/x/oauth2 v0.13.0 k8s.io/apimachinery v0.28.2 ) @@ -28,6 +25,7 @@ require ( github.com/bgentry/speakeasy v0.1.0 // indirect github.com/cloudflare/circl v1.3.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/ettle/strcase v0.1.1 // indirect github.com/evanphx/json-patch v5.7.0+incompatible // indirect github.com/fatih/color v1.15.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect @@ -100,6 +98,7 @@ require ( go.opentelemetry.io/otel/trace v1.19.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.14.0 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/mod v0.13.0 // indirect golang.org/x/net v0.17.0 // indirect golang.org/x/sys v0.13.0 // indirect diff --git a/go.sum b/go.sum index e854985..30cc138 100644 --- a/go.sum +++ b/go.sum @@ -313,6 +313,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/nitrado/tfconv v1.0.0 h1:7RyeX8yY8NgHjVs5bwxv24BTLD4liXhKdpFgJ9hart0= +github.com/nitrado/tfconv v1.0.0/go.mod h1:ZMwx92uhhrCiBUKxk1xZtf8PMwIypTgA8tHkf7u+mok= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= diff --git a/internal/cmd/schema-gen/gen.go b/internal/cmd/schema-gen/gen.go index b187b1b..65f7d19 100644 --- a/internal/cmd/schema-gen/gen.go +++ b/internal/cmd/schema-gen/gen.go @@ -9,7 +9,7 @@ import ( "text/template" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/nitrado/terraform-provider-ec/internal/terra/schemagen" + "github.com/nitrado/tfconv/schemagen" ) // Generator is a struct Terraform schema generator. It converts structs diff --git a/internal/terra/expand.go b/internal/terra/expand.go deleted file mode 100644 index 1c2a2e2..0000000 --- a/internal/terra/expand.go +++ /dev/null @@ -1,158 +0,0 @@ -package terra - -import ( - "fmt" - "reflect" -) - -// Expand converts Terraform-formatted data into the given object. -func (c *Converter) Expand(v, obj any) error { - typ := reflect.TypeOf(obj) - if typ.Kind() != reflect.Ptr { - return fmt.Errorf("obj is not a pointer by %q", typ.String()) - } - - return c.expand(v, reflect.ValueOf(obj).Elem()) -} - -func (c *Converter) expand(a any, objVal reflect.Value) error { - switch val := a.(type) { - case []any: - if len(val) == 0 { - return nil - } - - if objVal.Type().Kind() == reflect.Slice { - return c.expandSlice(val, objVal) - } - - m, ok := val[0].(map[string]any) - if !ok { - return fmt.Errorf("struct type requires data to be a map[string]any") - } - - if objVal.Type().Kind() == reflect.Ptr && objVal.Type().Elem().Kind() != reflect.Struct && len(m) == 1 { - // This is a pointer value struct, unwrap it. - if v, ok := m["value"]; ok { - return c.expandPrimitive(v, objVal) - } - } - - return c.expandStruct(m, objVal) - case map[string]any: - if len(val) == 0 { - return nil - } - - if objVal.Type().Kind() == reflect.Map { - return c.expandMap(val, objVal) - } - - // It could be we have a slice of struct. - return c.expandStruct(val, objVal) - default: - return c.expandPrimitive(a, objVal) - } -} - -func (c *Converter) expandStruct(m map[string]any, objVal reflect.Value) error { - if objVal.Type().Kind() == reflect.Ptr { - if objVal.IsNil() { - objVal.Set(reflect.New(objVal.Type().Elem())) - } - objVal = objVal.Elem() - } - - t := objVal.Type() - if t.Kind() != reflect.Struct { - return fmt.Errorf("expected struct, got %s", t.String()) - } - - for i := 0; i < t.NumField(); i++ { - sf := t.Field(i) - name := c.resolveName(sf) - - val, found := m[name] - if !found { - continue - } - - if err := c.expand(val, objVal.Field(i)); err != nil { - return err - } - } - return nil -} - -func (c *Converter) expandSlice(a []any, objVal reflect.Value) error { - t := objVal.Type() - if t.Kind() != reflect.Slice { - return fmt.Errorf("expected slice, got %s", t.String()) - } - - l := len(a) - if objVal.Len() < l { - objVal.Set(reflect.MakeSlice(t, l, l)) - } - - for i, v := range a { - if err := c.expand(v, objVal.Index(i)); err != nil { - return err - } - } - return nil -} - -func (c *Converter) expandMap(m map[string]any, objVal reflect.Value) error { - t := objVal.Type() - if t.Kind() != reflect.Map { - return fmt.Errorf("expected map, got %s", t.String()) - } - - if objVal.IsNil() { - objVal.Set(reflect.MakeMap(t)) - } - - for k, v := range m { - val := reflect.New(t.Elem()).Elem() - if err := c.expand(v, val); err != nil { - return err - } - - objVal.SetMapIndex(reflect.ValueOf(k), val) - } - return nil -} - -func (c *Converter) expandPrimitive(v any, objVal reflect.Value) error { - if objVal.Type().Kind() == reflect.Ptr { - if objVal.IsNil() { - objVal.Set(reflect.New(objVal.Type().Elem())) - } - objVal = objVal.Elem() - } - - objTyp := objVal.Type() - - if con, ok := c.conversions[objTyp]; ok { - var err error - v, err = con.expand(v) - if err != nil { - return err - } - } - - vVal := reflect.ValueOf(v) - vTyp := vVal.Type() - - switch { - case vTyp.AssignableTo(objTyp): - objVal.Set(reflect.ValueOf(v)) - return nil - case vTyp.ConvertibleTo(objTyp): - objVal.Set(vVal.Convert(objTyp)) - return nil - default: - return fmt.Errorf("primitive of type %s not supported", objTyp.String()) - } -} diff --git a/internal/terra/expand_test.go b/internal/terra/expand_test.go deleted file mode 100644 index 5cc77a8..0000000 --- a/internal/terra/expand_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package terra_test - -import ( - "testing" - - "github.com/nitrado/terraform-provider-ec/internal/terra" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/api/resource" -) - -func newInt(i int) *int { - return &i -} - -func TestConverter_Expand(t *testing.T) { - c := terra.New() - c.Register(resource.Quantity{}, func(v any) (any, error) { - return resource.ParseQuantity(v.(string)) - }, func(v any) (any, error) { - q := v.(resource.Quantity) - return (&q).String(), nil - }) - - data := []any{map[string]any{ - "str": "test-str", - "alias": "test-alias", - "int": 1, - "float": 2.3, - "bool": true, - "slice": []any{map[string]any{ - "a": "test-t", - }, map[string]any{ - "a": "test-t-also", - }}, - "map": map[string]any{ - "foo": 4, - }, - "struct": []any{map[string]any{ - "a": "test-ptr-t", - "b": []any{map[string]any{ - "value": 16, - }}, - }}, - "q": "205m", - }} - - got := TestObject{} - err := c.Expand(data, &got) - - require.NoError(t, err) - want := TestObject{ - Str: "test-str", - Alias: StrAlias("test-alias"), - Int: 1, - Float: 2.3, - Bool: true, - Slice: []T{ - {A: "test-t"}, - {A: "test-t-also"}, - }, - Map: map[string]int{ - "foo": 4, - }, - Struct: &T{ - A: "test-ptr-t", - B: newInt(16), - C: nil, - }, - Q: resource.MustParse("205m"), - } - assert.Equal(t, want, got) -} diff --git a/internal/terra/flatten.go b/internal/terra/flatten.go deleted file mode 100644 index 9e57178..0000000 --- a/internal/terra/flatten.go +++ /dev/null @@ -1,190 +0,0 @@ -package terra - -import ( - "errors" - "fmt" - "reflect" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -// Flatten converts an object into a Terraform data structure using its schema. -func (c *Converter) Flatten(obj any, s map[string]*schema.Schema) (any, error) { - v := reflect.ValueOf(obj) - return c.flattenStruct(v, s) -} - -func (c *Converter) flatten(v reflect.Value, s *schema.Schema) (any, error) { - switch { - case s.Type == schema.TypeInvalid: - return nil, errors.New("invalid schema type") - case s.Type == schema.TypeList && s.MaxItems == 1 && isResource(s.Elem): - res := s.Elem.(*schema.Resource) - - if v.Type().Kind() == reflect.Ptr && v.Type().Elem().Kind() != reflect.Struct { - val, err := c.flattenPrimitive(v, res.Schema["value"]) - if err != nil { - return nil, err - } - return []any{map[string]any{"value": val}}, nil - } - - return c.flattenStruct(v, res.Schema) - case s.Type == schema.TypeList: - return c.flattenSlice(v, s) - case s.Type == schema.TypeMap: - return c.flattenMap(v, s) - default: - return c.flattenPrimitive(v, s) - } -} - -func (c *Converter) flattenSlice(v reflect.Value, s *schema.Schema) (any, error) { - t := v.Type() - if t.Kind() != reflect.Slice { - return nil, fmt.Errorf("expected slice, got %s", t.String()) - } - - if v.IsNil() || v.Len() == 0 { - return []any{}, nil - } - - var elemS *schema.Schema - switch { - case isSchema(s.Elem): - elemS = s.Elem.(*schema.Schema) - case isResource(s.Elem): - elemS = &schema.Schema{} - *elemS = *s - elemS.MaxItems = 1 - default: - // Do nothing. - } - - d := make([]any, v.Len()) - for i := 0; i < v.Len(); i++ { - a, err := c.flatten(v.Index(i), elemS) - if err != nil { - return nil, err - } - if isResource(s.Elem) { - // This is for the edge case where a slice of structs should - // not return a []any[]any. Unwind the second slice. - a = a.([]any)[0] - } - d[i] = a - } - return d, nil -} - -func (c *Converter) flattenMap(v reflect.Value, s *schema.Schema) (any, error) { - t := v.Type() - if t.Kind() != reflect.Map { - return nil, fmt.Errorf("expected map, got %s", t.String()) - } - - if v.Len() == 0 { - return map[string]any{}, nil - } - - elemS := s.Elem.(*schema.Schema) - - d := make(map[string]any, v.Len()) - for _, k := range v.MapKeys() { - a, err := c.flatten(v.MapIndex(k), elemS) - if err != nil { - return nil, err - } - - d[k.String()] = a - } - return d, nil -} - -func (c *Converter) flattenStruct(v reflect.Value, s map[string]*schema.Schema) (any, error) { - if v.Type().Kind() == reflect.Ptr { - v = v.Elem() - } - - t := v.Type() - if t.Kind() != reflect.Struct { - return nil, fmt.Errorf("expected struct, got %s", t.String()) - } - - d := make(map[string]any, t.NumField()) - for i := 0; i < t.NumField(); i++ { - sf := t.Field(i) - name := c.resolveName(sf) - - sch, found := s[name] - if !found { - continue - } - fv := v.Field(i) - if fv.IsZero() { - continue - } - - val, err := c.flatten(fv, sch) - if err != nil { - return nil, err - } - d[name] = val - } - return []any{d}, nil -} - -func (c *Converter) flattenPrimitive(v reflect.Value, s *schema.Schema) (any, error) { - if v.Type().Kind() == reflect.Ptr { - if v.IsNil() { - //nolint:nilnil // This is expected. - return nil, nil - } - v = v.Elem() - } - - t := v.Type() - - if con, ok := c.conversions[t]; ok { - obj, err := con.flatten(v.Interface()) - if err != nil { - return nil, err - } - v = reflect.ValueOf(obj) - t = v.Type() - } - - switch s.Type { - case schema.TypeString: - if t.Kind() == reflect.String { - return v.String(), nil - } - case schema.TypeInt: - switch t.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return int(v.Int()), nil - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - return int(v.Uint()), nil - } - case schema.TypeFloat: - switch t.Kind() { - case reflect.Float32, reflect.Float64: - return v.Float(), nil - } - case schema.TypeBool: - if t.Kind() == reflect.Bool { - return v.Bool(), nil - } - } - return nil, fmt.Errorf("unsupported primitve type %s for schema type %s", t.String(), s.Type.String()) -} - -func isResource(v any) bool { - _, ok := v.(*schema.Resource) - return ok -} - -func isSchema(v any) bool { - _, ok := v.(*schema.Schema) - return ok -} diff --git a/internal/terra/flatten_test.go b/internal/terra/flatten_test.go deleted file mode 100644 index db9eab8..0000000 --- a/internal/terra/flatten_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package terra_test - -import ( - "testing" - - "github.com/nitrado/terraform-provider-ec/internal/terra" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/api/resource" -) - -func TestConverter_Flatten(t *testing.T) { - c := terra.New() - c.Register(resource.Quantity{}, func(v any) (any, error) { - return resource.ParseQuantity(v.(string)) - }, func(v any) (any, error) { - q := v.(resource.Quantity) - return (&q).String(), nil - }) - - obj := TestObject{ - Str: "test-str", - Alias: StrAlias("test-alias"), - Int: 1, - Float: 2.3, - Bool: true, - Slice: []T{ - {A: "test-t"}, - {A: "test-t-also"}, - }, - Map: map[string]int{ - "foo": 4, - }, - Struct: &T{ - A: "test-ptr-t", - B: newInt(16), - C: nil, - }, - Q: resource.MustParse("205m"), - } - - got, err := c.Flatten(obj, testObjectSchema()) - - require.NoError(t, err) - want := []any{map[string]any{ - "str": "test-str", - "alias": "test-alias", - "int": 1, - "float": 2.3, - "bool": true, - "slice": []any{map[string]any{ - "a": "test-t", - }, map[string]any{ - "a": "test-t-also", - }}, - "map": map[string]any{ - "foo": 4, - }, - "struct": []any{map[string]any{ - "a": "test-ptr-t", - "b": []any{map[string]any{ - "value": 16, - }}, - }}, - "q": "205m", - }} - assert.Equal(t, want, got) -} diff --git a/internal/terra/schemagen/generator.go b/internal/terra/schemagen/generator.go deleted file mode 100644 index 24bbf02..0000000 --- a/internal/terra/schemagen/generator.go +++ /dev/null @@ -1,232 +0,0 @@ -package schemagen - -import ( - "bytes" - "errors" - "fmt" - "reflect" - "sort" - "strings" - "text/template" - - "github.com/ettle/strcase" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "golang.org/x/exp/maps" -) - -// DocsFunc is a function that returns the documentation for a struct field. -type DocsFunc = func(obj any, sf reflect.StructField) string - -// CustomizerFunc is a function used to customize a schema. -// It returns an optional function name, the amended kind and a bool determining -// if the field should be skipped. -type CustomizerFunc = func(obj any, sf *reflect.StructField, typ reflect.Type, s *schema.Schema) (string, reflect.Kind, bool) - -var errSkip = errors.New("skip") - -// Generator generates Terraform schemas. -type Generator struct { - docsFn DocsFunc - customizerFn CustomizerFunc - tag string -} - -// New returns a schema generator. -func New(docsFn DocsFunc, customizerFn CustomizerFunc, tag string) *Generator { - if tag == "" { - tag = "json" - } - if docsFn == nil { - docsFn = func(any, reflect.StructField) string { return "" } - } - if customizerFn == nil { - customizerFn = func(_ any, _ *reflect.StructField, typ reflect.Type, _ *schema.Schema) (string, reflect.Kind, bool) { - return "", typ.Kind(), true - } - } - - return &Generator{ - docsFn: docsFn, - customizerFn: customizerFn, - tag: tag, - } -} - -// Struct generates the schema from the given object. -func (g *Generator) Struct(obj any) (map[string]string, error) { - typ := reflect.TypeOf(obj) - if typ.Kind() == reflect.Ptr { - typ = typ.Elem() - } - if typ.Kind() != reflect.Struct { - return nil, errors.New("obj must be a struct or pointer to a struct") - } - - fields := make(map[string]string, typ.NumField()) - for i := 0; i < typ.NumField(); i++ { - sf := typ.Field(i) - - name := g.ResolveName(sf) - code, err := g.genField(name, obj, &sf, sf.Type, false) - if err != nil { - switch { - case errors.Is(err, errSkip): - continue - default: - return nil, err - } - } - - fields[strcase.ToSnake(name)] = code - } - return fields, nil -} - -//nolint:cyclop -func (g *Generator) genField(name string, obj any, sf *reflect.StructField, typ reflect.Type, isNested bool) (string, error) { - typ, isPtr := derefType(typ) - s := &schema.Schema{} - - fnName, kind, ok := g.customizerFn(obj, sf, typ, s) - if !ok { - return "", errSkip - } - - if sf != nil { - s.Description = g.docsFn(obj, *sf) - } - - var setFunc string - switch kind { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, - reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - s.Type = schema.TypeInt - case reflect.Float32, reflect.Float64: - s.Type = schema.TypeFloat - case reflect.String: - s.Type = schema.TypeString - case reflect.Bool: - s.Type = schema.TypeBool - case reflect.Slice: - s.Type = schema.TypeList - elem, err := g.genField("", obj, nil, typ.Elem(), true) - if err != nil { - return "", fmt.Errorf("generating elem for %q: %w", name, err) - } - s.Elem = elem - case reflect.Map: - s.Type = schema.TypeMap - elem, err := g.genField("", obj, nil, typ.Elem(), true) - if err != nil { - return "", fmt.Errorf("generating elem for %q: %w", name, err) - } - s.Elem = elem - case reflect.Struct: - s.Type = schema.TypeList - s.MaxItems = 1 - - var elem string - if fnName == "" { - elem = "&schema.Resource{\nSchema: map[string]*schema.Schema{\n" - - newObj := reflect.New(typ).Elem().Interface() - fields, err := g.Struct(newObj) - if err != nil { - return "", err - } - - fieldNames := maps.Keys(fields) - sort.Strings(fieldNames) - for _, fieldName := range fieldNames { - elem += fmt.Sprintf("%q: %s,\n", fieldName, fields[fieldName]) - } - elem += "},\n}" - } else { - elem = "&schema.Resource{Schema:" + fnName + "()}" - } - - if isNested { - return elem, nil - } - s.Elem = elem - default: - return "", fmt.Errorf("unsupported type %q for %q", typ.String(), name) - } - - if isPtr && - (s.Type == schema.TypeInt || s.Type == schema.TypeFloat || s.Type == schema.TypeString || s.Type == schema.TypeBool) { - // Terraform always sets a default value for all properties not set. In the pointer case, - // we can no longer tell if it is nil or set by the user to a default value. Instead a block should - // be created in this case. - ptrS := &schema.Schema{} - *ptrS = *s - - // Ensure we override all values. - s.Optional = false - s.Computed = false - s.Required = true - elem, err := genSchemaField(s, "", false) - if err != nil { - return "", err - } - - ptrS.Type = schema.TypeList - ptrS.MaxItems = 1 - ptrS.Default = nil - ptrS.Elem = "&schema.Resource{\nSchema: map[string]*schema.Schema{\n\"value\": " + elem + ",\n},\n}" - s = ptrS - } - - return genSchemaField(s, setFunc, isNested) -} - -// ResolveName resolves the name of a field. -func (g *Generator) ResolveName(sf reflect.StructField) string { - jsonName := sf.Tag.Get(g.tag) - if name, _, ok := strings.Cut(jsonName, ","); ok { - jsonName = name - } - if jsonName != "" && jsonName != "-" { - return jsonName - } - return sf.Name -} - -func genSchemaField(s *schema.Schema, setFunc string, isNested bool) (string, error) { - buf := bytes.NewBuffer([]byte{}) - err := schemaTmpl.Execute(buf, struct { - Schema *schema.Schema - SetFunc string - IsNested bool - }{ - Schema: s, - SetFunc: setFunc, - IsNested: isNested, - }) - if err != nil { - return "", err - } - - return buf.String(), nil -} - -func derefType(t reflect.Type) (reflect.Type, bool) { - if t.Kind() == reflect.Ptr { - return t.Elem(), true - } - return t, false -} - -var schemaTmpl = template.Must(template.New("schema").Parse( - `{{if .IsNested}}&schema.Schema{{end}}{{"{"}}{{if not .IsNested}} -{{end}}Type: schema.{{.Schema.Type}},{{if ne .Schema.Description ""}} -Description: {{printf "%q" .Schema.Description}},{{end}}{{if .Schema.Required}} -Required: {{.Schema.Required}},{{end}}{{if .Schema.Optional}} -Optional: {{.Schema.Optional}},{{end}}{{if .Schema.ForceNew}} -ForceNew: {{.Schema.ForceNew}},{{end}}{{if .Schema.Computed}} -Computed: {{.Schema.Computed}},{{end}}{{if gt .Schema.MaxItems 0}} -MaxItems: {{.Schema.MaxItems}},{{end}}{{if .Schema.Elem}} -Elem: {{.Schema.Elem}},{{end}}{{if ne .SetFunc ""}}{{if not .IsNested}} -{{end}}Set: {{.SetFunc}},{{end}}{{if not .IsNested}} -{{end}}{{"}"}}`, -)) diff --git a/internal/terra/schemagen/generator_test.go b/internal/terra/schemagen/generator_test.go deleted file mode 100644 index 4b43e41..0000000 --- a/internal/terra/schemagen/generator_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package schemagen_test - -import ( - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/nitrado/terraform-provider-ec/internal/terra/schemagen" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGenerator_Struct(t *testing.T) { - gen := schemagen.New(nil, nil, "") - - got, err := gen.Struct(&TestObject{}) - - require.NoError(t, err) - want := map[string]string{ - "float": "{\nType: schema.TypeFloat,\n}", - "int": "{\nType: schema.TypeInt,\n}", - "str": "{\nType: schema.TypeString,\n}", - "bool": "{\nType: schema.TypeBool,\n}", - "ptr_bool": "{\nType: schema.TypeList,\nMaxItems: 1,\nElem: &schema.Resource{\nSchema: map[string]*schema.Schema{\n\"value\": {\nType: schema.TypeBool,\nRequired: true,\n},\n},\n},\n}", - "slice": "{\nType: schema.TypeList,\nElem: &schema.Resource{\nSchema: map[string]*schema.Schema{\n\"a\": {\nType: schema.TypeString,\n},\n},\n},\n}", - "map": "{\nType: schema.TypeMap,\nElem: &schema.Schema{Type: schema.TypeInt,},\n}", - "struct": "{\nType: schema.TypeList,\nMaxItems: 1,\nElem: &schema.Resource{\nSchema: map[string]*schema.Schema{\n\"a\": {\nType: schema.TypeString,\n},\n},\n},\n}", - } - assert.Equal(t, want, got) -} - -func TestGenerator_StructUsesCustomFunction(t *testing.T) { - c := func(_ any, _ *reflect.StructField, typ reflect.Type, _ *schema.Schema) (string, reflect.Kind, bool) { - if typ == reflect.TypeOf(T{}) { - return "myFunc", typ.Kind(), true - } - return "", typ.Kind(), true - } - - gen := schemagen.New(nil, c, "") - - got, err := gen.Struct(S{}) - - require.NoError(t, err) - want := map[string]string{ - "struct": "{\nType: schema.TypeList,\nMaxItems: 1,\nElem: &schema.Resource{Schema:myFunc()},\n}", - } - assert.Equal(t, want, got) -} - -func TestGenerator_StructUsesCustomKind(t *testing.T) { - c := func(_ any, _ *reflect.StructField, typ reflect.Type, _ *schema.Schema) (string, reflect.Kind, bool) { - if typ == reflect.TypeOf(T{}) { - return "", reflect.String, true - } - return "", typ.Kind(), true - } - - gen := schemagen.New(nil, c, "") - - got, err := gen.Struct(S{}) - - require.NoError(t, err) - want := map[string]string{ - "struct": "{\nType: schema.TypeString,\n}", - } - assert.Equal(t, want, got) -} - -func TestGenerator_StructSkipsFields(t *testing.T) { - c := func(_ any, _ *reflect.StructField, typ reflect.Type, _ *schema.Schema) (string, reflect.Kind, bool) { - if typ == reflect.TypeOf(T{}) { - return "", reflect.String, false - } - return "", typ.Kind(), true - } - - gen := schemagen.New(nil, c, "") - - got, err := gen.Struct(S{}) - - require.NoError(t, err) - want := map[string]string{} - assert.Equal(t, want, got) -} - -type TestObject struct { - Str string `json:"str"` - Int int `json:"int"` - Float float64 `json:"float,omitempty"` - Bool bool `json:"bool"` - PtrBool *bool `json:"ptrBool"` - Slice []T `json:"slice"` - Map map[string]int `json:"map"` - Struct *T -} - -type S struct { - Struct T `json:"struct"` -} - -type T struct { - A string `json:"a"` -} diff --git a/internal/terra/terra.go b/internal/terra/terra.go deleted file mode 100644 index 2f53b4a..0000000 --- a/internal/terra/terra.go +++ /dev/null @@ -1,52 +0,0 @@ -package terra - -import ( - "reflect" - "strings" - - "github.com/ettle/strcase" -) - -// ConverterFn is a function that can convert a value. -type ConverterFn func(v any) (any, error) - -type conversion struct { - expand ConverterFn - flatten ConverterFn -} - -// Converter converts Terraform formatted data to and from -// object types. -type Converter struct { - tag string - - conversions map[reflect.Type]conversion -} - -// New returns a new converter. -func New() *Converter { - return &Converter{ - tag: "json", - conversions: map[reflect.Type]conversion{}, - } -} - -// Register registers a custom type conversion. -func (c *Converter) Register(v any, expand, flatten ConverterFn) { - t := reflect.TypeOf(v) - c.conversions[t] = conversion{ - expand: expand, - flatten: flatten, - } -} - -func (c *Converter) resolveName(sf reflect.StructField) string { - jsonName := sf.Tag.Get(c.tag) - if name, _, ok := strings.Cut(jsonName, ","); ok { - jsonName = name - } - if jsonName != "" && jsonName != "-" { - return strcase.ToSnake(jsonName) - } - return strcase.ToSnake(sf.Name) -} diff --git a/internal/terra/terra_test.go b/internal/terra/terra_test.go deleted file mode 100644 index 1c63efd..0000000 --- a/internal/terra/terra_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package terra_test - -import ( - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "k8s.io/apimachinery/pkg/api/resource" -) - -type TestObject struct { - Str string `json:"str"` - Alias StrAlias `json:"alias"` - Int int `json:"int"` - Float float64 `json:"float,omitempty"` - Bool bool `json:"bool"` - Slice []T `json:"slice"` - Map map[string]int `json:"map"` - Struct *T `json:"struct"` - Q resource.Quantity `json:"q"` -} - -type T struct { - A string `json:"a"` - B *int `json:"b"` - C *int `json:"c"` -} - -type StrAlias string - -func testObjectSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "alias": { - Type: schema.TypeString, - Optional: true, - }, - "bool": { - Type: schema.TypeBool, - Optional: true, - }, - "float": { - Type: schema.TypeFloat, - Optional: true, - }, - "int": { - Type: schema.TypeInt, - Optional: true, - }, - "map": { - Type: schema.TypeMap, - Optional: true, - Elem: &schema.Schema{Type: schema.TypeInt}, - }, - "q": { - Type: schema.TypeString, - Optional: true, - }, - "slice": { - Type: schema.TypeList, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "a": { - Type: schema.TypeString, - Optional: true, - }, - }, - }, - }, - "str": { - Type: schema.TypeString, - Optional: true, - }, - "struct": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "a": { - Type: schema.TypeString, - Optional: true, - }, - "b": { - Type: schema.TypeList, - MaxItems: 1, - Optional: true, - Elem: &schema.Resource{Schema: map[string]*schema.Schema{ - "value": { - Type: schema.TypeInt, - Required: true, - }, - }}, - }, - "c": { - Type: schema.TypeList, - MaxItems: 1, - Optional: true, - Elem: &schema.Resource{Schema: map[string]*schema.Schema{ - "value": { - Type: schema.TypeInt, - Required: true, - }, - }}, - }, - }, - }, - }, - } -}