Repository: ariga/atlas
Pull request #245 (2021-11-24 12:16:28)

Feat/hcl interfaces
This PR adds support for reading from `schemaspec.Resource` trees into structs that contain slices of interface types. 

### Example 

Consider this block of HCL:
```hcl
zoo "ramat_gan" {
	lion "simba" {
		friend = "rafiki"
	}
	parrot "iago" {
		boss = "jafar"
	}
}
```

Both `lion` and `parrot` represent animals, and we would like to read them into a struct that contains an `[]Animal` slice such as:

```go
type (
  Animal interface {
	  animal()
  }
  Parrot struct {
	  Animal
	  Boss string `spec:"boss"`
  }
  Lion struct {
	  Animal
	  Friend string `spec:"friend"`
  }
  Zoo struct {
	  Animals []Animal `spec:""`
  }
)
```


Patch:
diff --git a/doc/md/CLI/getting_started.mdx b/doc/md/CLI/getting_started.mdx
index 9c6d4dab..68811392 100644
--- a/doc/md/CLI/getting_started.mdx
+++ b/doc/md/CLI/getting_started.mdx
@@ -85,4 +85,66 @@ Move the atlas binary to a file location on your system PATH.
 
 
 </TabItem>
-</Tabs>
\ No newline at end of file
+</Tabs>
+
+## Schema Inspection
+
+<Tabs
+defaultValue="mysql"
+values={[
+{label: 'MySQL', value: 'mysql'},
+{label: 'PostgreSQL', value: 'postgres'},
+]}>
+<TabItem value="mysql">
+
+Inspect and save output to a schema file.
+```shell
+atlas schema inspect -d "mysql://root:pass@tcp(localhost:3306)/example" >> atlas.hcl
+```
+
+</TabItem>
+<TabItem value="postgres">
+
+Inspect and save output to a schema file.
+```shell
+atlas schema inspect -d "postgres://root:pass@0.0.0.0:5432/example?sslmode=disable" >> atlas.hcl
+```
+
+:::caution
+
+sslmode disable is not recommended in prod.
+
+:::
+
+</TabItem>
+</Tabs>
+
+## Apply change to Schema
+
+<Tabs
+defaultValue="mysql"
+values={[
+{label: 'MySQL', value: 'mysql'},
+{label: 'PostgreSQL', value: 'postgres'},
+]}>
+<TabItem value="mysql">
+
+```shell
+atlas schema apply -d "mysql://root:pass@tcp(localhost:3306)/example" -f atlas.hcl
+```
+
+</TabItem>
+<TabItem value="postgres">
+
+```shell
+atlas schema apply -d "postgres://root:pass@0.0.0.0:5432/example?sslmode=disable" -f atlas.hcl
+```
+
+:::caution
+
+sslmode disable is not recommended in prod.
+
+:::
+
+</TabItem>
+</Tabs>
diff --git a/schema/schemaspec/extension.go b/schema/schemaspec/extension.go
index 700c6c4f..33142f42 100644
--- a/schema/schemaspec/extension.go
+++ b/schema/schemaspec/extension.go
@@ -53,6 +53,20 @@ func (r registry) lookup(ext interface{}) (string, bool) {
 	return "", false
 }
 
+// implementers returns a slice of the names of the extensions that implement i.
+func (r registry) implementers(i reflect.Type) ([]string, error) {
+	if i.Kind() != reflect.Interface {
+		return nil, fmt.Errorf("schemaspec: expected interface got %s", i.Kind())
+	}
+	var names []string
+	for name, typ := range r {
+		if reflect.TypeOf(typ).Implements(i) {
+			names = append(names, name)
+		}
+	}
+	return names, nil
+}
+
 // Register records the type of ext in the global extension registry.
 // If Register is called twice with the same name or if ext is nil,
 // it panics.
@@ -94,6 +108,53 @@ func (r *Resource) As(target interface{}) error {
 				return err
 			}
 			delete(existingAttrs, attr.K)
+		case ft.isInterfaceSlice():
+			elem := field.Type().Elem()
+			impls, err := extensions.implementers(elem)
+			if err != nil {
+				return err
+			}
+			children := childrenOfType(r, impls...)
+			slc := reflect.MakeSlice(reflect.SliceOf(elem), 0, len(children))
+			for _, c := range children {
+				typ, ok := extensions[c.Type]
+				if !ok {
+					return fmt.Errorf("extension %q not registered", c.Type)
+				}
+				n := reflect.New(reflect.TypeOf(typ).Elem())
+				ext := n.Interface()
+				if err := c.As(ext); err != nil {
+					return err
+				}
+				slc = reflect.Append(slc, reflect.ValueOf(ext))
+			}
+			field.Set(slc)
+			for _, i := range impls {
+				delete(existingChildren, i)
+			}
+		case ft.isInterface():
+			impls, err := extensions.implementers(ft.typ)
+			if err != nil {
+				return err
+			}
+			children := childrenOfType(r, impls...)
+			if len(children) == 0 {
+				continue
+			}
+			if len(children) > 1 {
+				return fmt.Errorf("more than one blocks implement %q", ft.typ)
+			}
+			c := children[0]
+			typ, ok := extensions[c.Type]
+			if !ok {
+				return fmt.Errorf("extension %q not registered", c.Type)
+			}
+			n := reflect.New(reflect.TypeOf(typ).Elem())
+			ext := n.Interface()
+			if err := c.As(ext); err != nil {
+				return err
+			}
+			field.Set(n)
 		case isResourceSlice(field.Type()):
 			if err := setChildSlice(field, childrenOfType(r, ft.tag)); err != nil {
 				return err
@@ -391,6 +452,7 @@ func specFields(ext interface{}) []fieldTag {
 			field:  f.Name,
 			tag:    lookup,
 			isName: len(parts) > 1 && parts[1] == "name",
+			typ:    f.Type,
 		})
 	}
 	return fields
@@ -399,13 +461,24 @@ func specFields(ext interface{}) []fieldTag {
 type fieldTag struct {
 	field, tag string
 	isName     bool
+	typ        reflect.Type
+}
+
+func (f fieldTag) isInterfaceSlice() bool {
+	return f.typ.Kind() == reflect.Slice && f.typ.Elem().Kind() == reflect.Interface
 }
 
-func childrenOfType(r *Resource, typ string) []*Resource {
+func (f fieldTag) isInterface() bool {
+	return f.typ.Kind() == reflect.Interface
+}
+
+func childrenOfType(r *Resource, types ...string) []*Resource {
 	var out []*Resource
 	for _, c := range r.Children {
-		if c.Type == typ {
-			out = append(out, c)
+		for _, typ := range types {
+			if c.Type == typ {
+				out = append(out, c)
+			}
 		}
 	}
 	return out


Test patch:
diff --git a/schema/schemaspec/schemahcl/hcl_test.go b/schema/schemaspec/schemahcl/hcl_test.go
index a7cd75bf..06271770 100644
--- a/schema/schemaspec/schemahcl/hcl_test.go
+++ b/schema/schemaspec/schemahcl/hcl_test.go
@@ -5,6 +5,7 @@ import (
 	"log"
 	"testing"
 
+	"ariga.io/atlas/schema/schemaspec"
 	"github.com/stretchr/testify/require"
 )
 
@@ -143,3 +144,79 @@ func ExampleMarshal() {
 	//   y = 1
 	// }
 }
+
+func TestInterface(t *testing.T) {
+	type (
+		Animal interface {
+			animal()
+		}
+		Parrot struct {
+			Animal
+			Name string `spec:",name"`
+			Boss string `spec:"boss"`
+		}
+		Lion struct {
+			Animal
+			Name   string `spec:",name"`
+			Friend string `spec:"friend"`
+		}
+		Zoo struct {
+			Animals []Animal `spec:""`
+		}
+		Cast struct {
+			Animal Animal `spec:""`
+		}
+	)
+	schemaspec.Register("lion", &Lion{})
+	schemaspec.Register("parrot", &Parrot{})
+	t.Run("single", func(t *testing.T) {
+		f := `
+cast "lion_king" {
+	lion "simba" {
+		friend = "rafiki"
+	}
+}
+`
+		var test struct {
+			Cast *Cast `spec:"cast"`
+		}
+		err := Unmarshal([]byte(f), &test)
+		require.NoError(t, err)
+		require.EqualValues(t, &Cast{
+			Animal: &Lion{
+				Name:   "simba",
+				Friend: "rafiki",
+			},
+		}, test.Cast)
+	})
+	t.Run("slice", func(t *testing.T) {
+		f := `
+zoo "ramat_gan" {
+	lion "simba" {
+		friend = "rafiki"
+	}
+	parrot "iago" {
+		boss = "jafar"
+	}
+}
+`
+		var test struct {
+			Zoo *Zoo `spec:"zoo"`
+		}
+		err := Unmarshal([]byte(f), &test)
+		require.NoError(t, err)
+		require.EqualValues(t, &Zoo{
+			Animals: []Animal{
+				&Lion{
+					Name:   "simba",
+					Friend: "rafiki",
+				},
+				&Parrot{
+					Name: "iago",
+					Boss: "jafar",
+				},
+			},
+		}, test.Zoo)
+	})
+
+}