Repository: ariga/atlas
Pull request #299 (2021-12-11 21:14:53)

Feat/convert schema type
Stacked on #298 

Adding method to convert schema.Type the a schemaspec.Type generically, using reflection. 
In the future, should probably be solved by expanding the schema.Type interface.


Patch:
diff --git a/go.mod b/go.mod
index 31eb89e1..33019ac6 100644
--- a/go.mod
+++ b/go.mod
@@ -24,6 +24,7 @@ require (
 	github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
 	github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/go-openapi/inflect v0.19.0 // indirect
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 	github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
diff --git a/go.sum b/go.sum
index b57040a1..e31765bc 100644
--- a/go.sum
+++ b/go.sum
@@ -84,6 +84,8 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
+github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
 github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
 github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
diff --git a/schema/schemaspec/spec.go b/schema/schemaspec/spec.go
index 278e5eba..75a4e50a 100644
--- a/schema/schemaspec/spec.go
+++ b/schema/schemaspec/spec.go
@@ -77,7 +77,7 @@ type (
 	// TypeAttr describes an attribute of a TypeSpec, for example `varchar` fields
 	// can have a `size` attribute.
 	TypeAttr struct {
-		Name     string
+		Name     string // Name should be a snake_case of related the schema.Type struct field.
 		Kind     reflect.Kind
 		Required bool
 	}
@@ -235,6 +235,16 @@ func BoolVal(v Value) (bool, error) {
 	return b, nil
 }
 
+// Attr returns a TypeAttr by name and reports if one was found.
+func (s *TypeSpec) Attr(name string) (*TypeAttr, bool) {
+	for _, ta := range s.Attributes {
+		if ta.Name == name {
+			return ta, true
+		}
+	}
+	return nil, false
+}
+
 func (*LiteralValue) val() {}
 func (*ListValue) val()    {}
 func (*Ref) val()          {}
diff --git a/sql/internal/specutil/types.go b/sql/internal/specutil/types.go
index 495c8036..84d83243 100644
--- a/sql/internal/specutil/types.go
+++ b/sql/internal/specutil/types.go
@@ -3,9 +3,13 @@ package specutil
 import (
 	"errors"
 	"fmt"
+	"reflect"
+	"strconv"
 	"strings"
 
 	"ariga.io/atlas/schema/schemaspec"
+	"ariga.io/atlas/sql/schema"
+	"github.com/go-openapi/inflect"
 )
 
 // PrintType returns the string representation of a column type which can be parsed
@@ -81,3 +85,75 @@ func (r *TypeRegistry) Find(t string) (*schemaspec.TypeSpec, bool) {
 	return nil, false
 }
 
+// Convert converts the schema.Type to a *schemaspec.Type.
+func (r *TypeRegistry) Convert(typ schema.Type) (*schemaspec.Type, error) {
+	s := &schemaspec.Type{}
+	rv := reflect.ValueOf(typ)
+	if rv.Kind() == reflect.Ptr {
+		rv = rv.Elem()
+	}
+	if !rv.IsValid() {
+		return nil, errors.New("specutil: invalid schema.Type on Convert")
+	}
+	tf := rv.FieldByName("T")
+	if !tf.IsValid() {
+		return nil, fmt.Errorf("specutil: cannot convert schema.Type without T field for %T", typ)
+	}
+	if tf.Kind() != reflect.String {
+		return nil, fmt.Errorf("specutil: cannot convert non-string T field for %T", typ)
+	}
+	s.T = tf.String()
+	typeSpec, ok := r.Find(s.T)
+	if !ok {
+		return nil, fmt.Errorf("specutil: type %q not found in registry", s.T)
+	}
+	for _, attr := range typeSpec.Attributes {
+		n := inflect.Camelize(attr.Name)
+		field := rv.FieldByName(n)
+		if !field.IsValid() {
+			return nil, fmt.Errorf("invalid field name %q for attr %q on type spec %q", n, attr.Name, typeSpec.T)
+		}
+		if field.Kind() != attr.Kind {
+			return nil, errors.New("incompatible kinds on typespec attr and typefield")
+		}
+		switch attr.Kind {
+		case reflect.Int:
+			i := strconv.Itoa(int(field.Int()))
+			s.Attributes = append(s.Attributes, LitAttr(attr.Name, i))
+		case reflect.Bool:
+			b := strconv.FormatBool(field.Bool())
+			s.Attributes = append(s.Attributes, LitAttr(attr.Name, b))
+		case reflect.Slice:
+			lits := make([]string, 0, field.Len())
+			for i := 0; i < field.Len(); i++ {
+				fi := field.Index(i)
+				if fi.Kind() != reflect.String {
+					return nil, errors.New("specutil: only string slices currently supported")
+				}
+				lits = append(lits, strconv.Quote(fi.String()))
+			}
+			s.Attributes = append(s.Attributes, ListAttr(attr.Name, lits...))
+		default:
+			return nil, fmt.Errorf("specutil: unsupported attr kind %s for attribute %q of %q", attr.Kind, attr.Name, typeSpec.Name)
+		}
+	}
+	return s, nil
+}
+
+// TypeSpec returns a TypeSpec with the provided name.
+func TypeSpec(name string, attrs ...*schemaspec.TypeAttr) *schemaspec.TypeSpec {
+	return &schemaspec.TypeSpec{
+		Name:       name,
+		T:          name,
+		Attributes: attrs,
+	}
+}
+
+// SizeTypeAttr returns a TypeAttr for a size attribute.
+func SizeTypeAttr(required bool) *schemaspec.TypeAttr {
+	return &schemaspec.TypeAttr{
+		Name:     "size",
+		Kind:     reflect.Int,
+		Required: required,
+	}
+}


Test patch:
diff --git a/sql/internal/specutil/types_test.go b/sql/internal/specutil/types_test.go
index 36e81cff..7b803bd3 100644
--- a/sql/internal/specutil/types_test.go
+++ b/sql/internal/specutil/types_test.go
@@ -5,6 +5,7 @@ import (
 	"testing"
 
 	"ariga.io/atlas/schema/schemaspec"
+	"ariga.io/atlas/sql/schema"
 	"github.com/stretchr/testify/require"
 )
 
@@ -13,7 +14,7 @@ func TestTypePrint(t *testing.T) {
 		Name: "int",
 		T:    "int",
 		Attributes: []*schemaspec.TypeAttr{
-			{Name: "unsigned", Kind: reflect.Bool, Required: false},
+			unsignedTypeAttr(),
 		},
 	}
 	for _, tt := range []struct {
@@ -62,3 +63,83 @@ func TestRegistry(t *testing.T) {
 	require.True(t, ok)
 	require.EqualValues(t, spec, text)
 }
+
+func TestRegistryConvert(t *testing.T) {
+	r := &TypeRegistry{}
+	err := r.Register(
+		TypeSpec("varchar", SizeTypeAttr(true)),
+		TypeSpec("int", unsignedTypeAttr()),
+		TypeSpec(
+			"decimal",
+			&schemaspec.TypeAttr{
+				Name:     "precision",
+				Kind:     reflect.Int,
+				Required: false,
+			},
+			&schemaspec.TypeAttr{
+				Name:     "scale",
+				Kind:     reflect.Int,
+				Required: false,
+			},
+		),
+		TypeSpec("enum", &schemaspec.TypeAttr{
+			Name:     "values",
+			Kind:     reflect.Slice,
+			Required: true,
+		}),
+	)
+	require.NoError(t, err)
+	for _, tt := range []struct {
+		typ         schema.Type
+		expected    *schemaspec.Type
+		expectedErr string
+	}{
+		{
+			typ:      &schema.StringType{T: "varchar", Size: 255},
+			expected: &schemaspec.Type{T: "varchar", Attributes: []*schemaspec.Attr{LitAttr("size", "255")}},
+		},
+		{
+			typ:      &schema.IntegerType{T: "int", Unsigned: true},
+			expected: &schemaspec.Type{T: "int", Attributes: []*schemaspec.Attr{LitAttr("unsigned", "true")}},
+		},
+		{
+			typ:      &schema.IntegerType{T: "int", Unsigned: true},
+			expected: &schemaspec.Type{T: "int", Attributes: []*schemaspec.Attr{LitAttr("unsigned", "true")}},
+		},
+		{
+			typ: &schema.DecimalType{T: "decimal", Precision: 10, Scale: 2},
+			expected: &schemaspec.Type{T: "decimal", Attributes: []*schemaspec.Attr{
+				LitAttr("precision", "10"),
+				LitAttr("scale", "2"),
+			}},
+		},
+		{
+			typ: &schema.EnumType{T: "enum", Values: []string{"on", "off"}},
+			expected: &schemaspec.Type{T: "enum", Attributes: []*schemaspec.Attr{
+				ListAttr("values", `"on"`, `"off"`),
+			}},
+		},
+		{
+			typ:         nil,
+			expected:    &schemaspec.Type{},
+			expectedErr: "specutil: invalid schema.Type on Convert",
+		},
+	} {
+		t.Run(tt.expected.T, func(t *testing.T) {
+			convert, err := r.Convert(tt.typ)
+			if tt.expectedErr != "" {
+				require.EqualError(t, err, tt.expectedErr)
+				return
+			}
+			require.NoError(t, err)
+			require.EqualValues(t, tt.expected, convert)
+		})
+	}
+}
+
+func unsignedTypeAttr() *schemaspec.TypeAttr {
+	return &schemaspec.TypeAttr{
+		Name: "unsigned",
+		Kind: reflect.Bool,
+	}
+}