Repository: ariga/atlas
Pull request #1210 (2022-10-19 10:19:38)

schemahcl: support decoding struct fields


Patch:
diff --git a/schemahcl/extension.go b/schemahcl/extension.go
index 472f9e78..04b9acb6 100644
--- a/schemahcl/extension.go
+++ b/schemahcl/extension.go
@@ -91,6 +91,11 @@ func (r *Resource) As(target any) error {
 	if err := validateStructPtr(target); err != nil {
 		return err
 	}
+	return r.as(target)
+}
+
+// As reads the attributes and children resources of the resource into the target struct.
+func (r *Resource) as(target any) error {
 	existingAttrs, existingChildren := existingElements(r)
 	var seenName, seenQualifier bool
 	v := reflect.ValueOf(target).Elem()
@@ -133,7 +138,7 @@ func (r *Resource) As(target any) error {
 				}
 				n := reflect.New(reflect.TypeOf(typ).Elem())
 				ext := n.Interface()
-				if err := c.As(ext); err != nil {
+				if err := c.as(ext); err != nil {
 					return err
 				}
 				slc = reflect.Append(slc, reflect.ValueOf(ext))
@@ -161,7 +166,7 @@ func (r *Resource) As(target any) error {
 			}
 			n := reflect.New(reflect.TypeOf(typ).Elem())
 			ext := n.Interface()
-			if err := c.As(ext); err != nil {
+			if err := c.as(ext); err != nil {
 				return err
 			}
 			field.Set(n)
@@ -175,11 +180,24 @@ func (r *Resource) As(target any) error {
 			if len(c) == 0 {
 				continue
 			}
-			res := c[0]
-			n := reflect.New(field.Type().Elem())
-			ext := n.Interface()
-			if err := res.As(ext); err != nil {
-				return err
+			var (
+				res = c[0]
+				n   reflect.Value
+			)
+			switch field.Type().Kind() {
+			case reflect.Struct:
+				n = reflect.New(field.Type())
+				ext := n.Interface()
+				if err := res.as(ext); err != nil {
+					return err
+				}
+				n = n.Elem()
+			case reflect.Pointer:
+				n = reflect.New(field.Type().Elem())
+				ext := n.Interface()
+				if err := res.as(ext); err != nil {
+					return err
+				}
 			}
 			field.Set(n)
 			delete(existingChildren, ft.tag)
@@ -262,7 +280,7 @@ func setChildSlice(field reflect.Value, children []*Resource) error {
 	for _, c := range children {
 		n := reflect.New(typ.Elem())
 		ext := n.Interface()
-		if err := c.As(ext); err != nil {
+		if err := c.as(ext); err != nil {
 			return err
 		}
 		slc = reflect.Append(slc, reflect.ValueOf(ext))
@@ -406,7 +424,7 @@ func (r *Resource) Scan(ext any) error {
 	if lookup, ok := extensions.lookup(ext); ok {
 		r.Type = lookup
 	}
-	v := reflect.ValueOf(ext).Elem()
+	v := indirect(reflect.ValueOf(ext))
 	for _, ft := range specFields(ext) {
 		field := v.FieldByName(ft.Name)
 		switch {
@@ -432,7 +450,7 @@ func (r *Resource) Scan(ext any) error {
 				r.Children = append(r.Children, child)
 			}
 		case isSingleResource(field.Type()):
-			if field.IsNil() {
+			if k := field.Kind(); k == reflect.Struct && field.IsZero() || k == reflect.Pointer && field.IsNil() {
 				continue
 			}
 			ext := field.Interface()
@@ -560,10 +578,10 @@ func scanSliceAttr(key string, r *Resource, field reflect.Value) error {
 // specFields uses reflection to find struct fields that are tagged with "spec"
 // and returns a list of mappings from the tag to the field name.
 func specFields(ext any) []fieldDesc {
-	t := reflect.TypeOf(ext)
+	t := indirect(reflect.TypeOf(ext))
 	var fields []fieldDesc
-	for i := 0; i < t.Elem().NumField(); i++ {
-		f := t.Elem().Field(i)
+	for i := 0; i < t.NumField(); i++ {
+		f := t.Field(i)
 		tag, ok := f.Tag.Lookup("spec")
 		if !ok {
 			continue
@@ -637,15 +655,14 @@ func childrenOfType(r *Resource, types ...string) []*Resource {
 }
 
 func isSingleResource(t reflect.Type) bool {
-	if t.Kind() != reflect.Ptr {
-		return false
+	if t.Kind() == reflect.Ptr {
+		t = t.Elem()
 	}
-	elem := t.Elem()
-	if elem.Kind() != reflect.Struct {
+	if t.Kind() != reflect.Struct {
 		return false
 	}
-	for i := 0; i < elem.NumField(); i++ {
-		f := elem.Field(i)
+	for i := 0; i < t.NumField(); i++ {
+		f := t.Field(i)
 		if _, ok := f.Tag.Lookup("spec"); ok {
 			return true
 		}
@@ -667,3 +684,16 @@ func hasAttr(r *Resource, name string) bool {
 	_, ok := r.Attr(name)
 	return ok
 }
+
+type rtype[T any] interface {
+	Elem() T
+	Kind() reflect.Kind
+}
+
+// indirect returns the type at the end of indirection.
+func indirect[T rtype[T]](t T) T {
+	for t.Kind() == reflect.Ptr {
+		t = t.Elem()
+	}
+	return t
+}


Test patch:
diff --git a/schemahcl/hcl_test.go b/schemahcl/hcl_test.go
index 1b8b618b..56d9fa95 100644
--- a/schemahcl/hcl_test.go
+++ b/schemahcl/hcl_test.go
@@ -50,6 +50,10 @@ func TestResource(t *testing.T) {
   handler {
     active = true
     addr   = ":8080"
+    tag {
+      name  = "name"
+      value = "value"
+    }
   }
 }
 `
@@ -57,6 +61,10 @@ func TestResource(t *testing.T) {
 		Handler struct {
 			Active bool   `spec:"active"`
 			Addr   string `spec:"addr"`
+			Tag    struct {
+				Name  string `spec:"name"`
+				Value string `spec:"value"`
+			} `spec:"tag"`
 		}
 
 		Endpoint struct {
@@ -80,6 +88,10 @@ func TestResource(t *testing.T) {
 		Handler: &Handler{
 			Active: true,
 			Addr:   ":8080",
+			Tag: struct {
+				Name  string `spec:"name"`
+				Value string `spec:"value"`
+			}{Name: "name", Value: "value"},
 		},
 	}
 	require.EqualValues(t, expected, test.Endpoints[0])