Repository: ariga/atlas
Pull request #179 (2021-10-10 11:04:42)

schema/schemaspec/schemahcl: support Marshal
also fixing a big in schemaspec.Resource `Scan` where *Ref fields were not properly supported. 


Patch:
diff --git a/schema/schemaspec/extension.go b/schema/schemaspec/extension.go
index 338ea1cf..60f82f1d 100644
--- a/schema/schemaspec/extension.go
+++ b/schema/schemaspec/extension.go
@@ -258,14 +258,9 @@ func (r *Resource) Scan(ext interface{}) error {
 			if field.IsNil() {
 				continue
 			}
-			if field.Elem().Type() != reflect.TypeOf(LiteralValue{}) {
-				return fmt.Errorf("schemaspec: pointer to non LiteralValue")
+			if err := scanPtr(ft.tag, r, field); err != nil {
+				return err
 			}
-			v := field.Elem().FieldByName("V").String()
-			r.SetAttr(&Attr{
-				K: ft.tag,
-				V: &LiteralValue{V: v},
-			})
 		default:
 			if err := scanAttr(ft.tag, r, field); err != nil {
 				return err
@@ -284,6 +279,20 @@ func (r *Resource) Scan(ext interface{}) error {
 	return nil
 }
 
+func scanPtr(key string, r *Resource, field reflect.Value) error {
+	attr := &Attr{K: key}
+	switch e := field.Elem(); e.Type() {
+	case reflect.TypeOf(LiteralValue{}):
+		attr.V = &LiteralValue{V: e.FieldByName("V").String()}
+	case reflect.TypeOf(Ref{}):
+		attr.V = &Ref{V: e.FieldByName("V").String()}
+	default:
+		return fmt.Errorf("schemaspec: unsupported pointer to %s", e)
+	}
+	r.SetAttr(attr)
+	return nil
+}
+
 func scanAttr(key string, r *Resource, field reflect.Value) error {
 	var lit string
 	switch field.Kind() {
diff --git a/schema/schemaspec/schemahcl/hcl.go b/schema/schemaspec/schemahcl/hcl.go
index a2e6e028..4f1ee285 100644
--- a/schema/schemaspec/schemahcl/hcl.go
+++ b/schema/schemaspec/schemahcl/hcl.go
@@ -17,6 +17,15 @@ import (
 	"github.com/zclconf/go-cty/cty"
 )
 
+// Marshal returns the Atlas HCL encoding of v.
+func Marshal(v interface{}) ([]byte, error) {
+	r := &schemaspec.Resource{}
+	if err := r.Scan(v); err != nil {
+		return nil, fmt.Errorf("schemahcl: failed scanning %T to resource: %w", v, err)
+	}
+	return encode(r)
+}
+
 // Unmarshal parses the Atlas HCL-encoded data and stores the result in the target.
 func Unmarshal(data []byte, v interface{}) error {
 	spec, err := decode(data)
@@ -166,9 +175,9 @@ func toResource(ctx *hcl.EvalContext, block *hclsyntax.Block) (*schemaspec.Resou
 	return spec, nil
 }
 
-// Encode encodes the give *schemaspec.Resource into a byte slice containing an Atlas HCL
+// encode encodes the give *schemaspec.Resource into a byte slice containing an Atlas HCL
 // document representing it.
-func Encode(r *schemaspec.Resource) ([]byte, error) {
+func encode(r *schemaspec.Resource) ([]byte, error) {
 	f := hclwrite.NewFile()
 	body := f.Body()
 	for _, attr := range r.Attrs {
@@ -187,7 +196,11 @@ func Encode(r *schemaspec.Resource) ([]byte, error) {
 }
 
 func writeResource(b *schemaspec.Resource, body *hclwrite.Body) error {
-	blk := body.AppendNewBlock(b.Type, []string{b.Name})
+	var labels []string
+	if b.Name != "" {
+		labels = append(labels, b.Name)
+	}
+	blk := body.AppendNewBlock(b.Type, labels)
 	nb := blk.Body()
 	for _, attr := range b.Attrs {
 		if err := writeAttr(attr, nb); err != nil {


Test patch:
diff --git a/schema/schemaspec/extension_test.go b/schema/schemaspec/extension_test.go
index ba5e8a74..bf19d71e 100644
--- a/schema/schemaspec/extension_test.go
+++ b/schema/schemaspec/extension_test.go
@@ -127,6 +127,32 @@ func TestNested(t *testing.T) {
 	require.EqualValues(t, pet, scan)
 }
 
+func TestRef(t *testing.T) {
+	type A struct {
+		Name string          `spec:",name"`
+		User *schemaspec.Ref `spec:"user"`
+	}
+	schemaspec.Register("res", &A{})
+	resource := &schemaspec.Resource{
+		Name: "x",
+		Type: "res",
+		Attrs: []*schemaspec.Attr{
+			{
+				K: "user",
+				V: &schemaspec.Ref{V: "$user.rotemtam"},
+			},
+		},
+	}
+	tgt := A{}
+	err := resource.As(&tgt)
+	require.NoError(t, err)
+	require.EqualValues(t, &schemaspec.Ref{V: "$user.rotemtam"}, tgt.User)
+	scan := &schemaspec.Resource{}
+	err = scan.Scan(&tgt)
+	require.NoError(t, err)
+	require.EqualValues(t, resource, scan)
+}
+
 func TestListRef(t *testing.T) {
 	type A struct {
 		Name  string            `spec:",name"`
diff --git a/schema/schemaspec/schemahcl/context_test.go b/schema/schemaspec/schemahcl/context_test.go
index 9980e1a1..1362450e 100644
--- a/schema/schemaspec/schemahcl/context_test.go
+++ b/schema/schemaspec/schemahcl/context_test.go
@@ -54,6 +54,7 @@ config "defaults" {
 		Endpoints []*Endpoint `spec:"endpoint"`
 	}
 	err := Unmarshal([]byte(f), &test)
+	require.NoError(t, err)
 	require.EqualValues(t, []*Endpoint{
 		{
 			Name:      "home",
@@ -69,33 +70,6 @@ config "defaults" {
 			Addr: "127.0.0.1:8082",
 		},
 	}, test.Endpoints)
-	require.NoError(t, err)
-	res, err := decode([]byte(f))
-	require.NoError(t, err)
-	home := res.Children[2]
-	attr, ok := home.Attr("addr")
-	require.True(t, ok)
-	s, err := attr.String()
-	require.NoError(t, err)
-	require.EqualValues(t, "127.0.0.1:8081", s)
-
-	attr, ok = home.Attr("timeout_ms")
-	require.True(t, ok)
-	timeoutMs, err := attr.Int()
-	require.NoError(t, err)
-	require.EqualValues(t, 10, timeoutMs)
-
-	attr, ok = home.Attr("retry")
-	require.True(t, ok)
-	retry, err := attr.Bool()
-	require.NoError(t, err)
-	require.EqualValues(t, false, retry)
-
-	attr, ok = home.Attr("description")
-	require.True(t, ok)
-	interpolated, err := attr.String()
-	require.NoError(t, err)
-	require.EqualValues(t, "default: generic", interpolated)
 }
 
 func TestNestedReferences(t *testing.T) {
@@ -139,13 +113,11 @@ country "israel" {
 }
 
 func TestBlockReference(t *testing.T) {
-	f := `
-person "jon" {
-	
+	f := `person "jon" {
 }
 pet "garfield" {
-	type = "cat"
-	owner = person.jon
+  type  = "cat"
+  owner = person.jon
 }
 `
 	type (
@@ -169,6 +141,9 @@ pet "garfield" {
 		Type:  "cat",
 		Owner: &schemaspec.Ref{V: "$person.jon"},
 	}, test.Pets[0])
+	marshal, err := Marshal(&test)
+	require.NoError(t, err)
+	require.EqualValues(t, f, string(marshal))
 }
 
 func TestListRefs(t *testing.T) {
diff --git a/schema/schemaspec/schemahcl/hcl_test.go b/schema/schemaspec/schemahcl/hcl_test.go
index c1a76696..6b3b255b 100644
--- a/schema/schemaspec/schemahcl/hcl_test.go
+++ b/schema/schemaspec/schemahcl/hcl_test.go
@@ -7,8 +7,7 @@ import (
 )
 
 func TestAttributes(t *testing.T) {
-	f := `
-i = 1
+	f := `i = 1
 b = true
 s = "hello, world"
 `
@@ -22,17 +21,19 @@ s = "hello, world"
 	require.EqualValues(t, 1, test.Int)
 	require.EqualValues(t, true, test.Bool)
 	require.EqualValues(t, "hello, world", test.Str)
+	marshal, err := Marshal(&test)
+	require.NoError(t, err)
+	require.EqualValues(t, f, string(marshal))
 }
 
 func TestResource(t *testing.T) {
-	f := `
-endpoint "/hello" {
-	handler {
-		active = true
-		addr = ":8080"
-	}
-	description = "the hello handler"
-	timeout_ms = 100
+	f := `endpoint "/hello" {
+  description = "the hello handler"
+  timeout_ms  = 100
+  handler {
+    active = true
+    addr   = ":8080"
+  }
 }
 `
 	type (
@@ -65,50 +66,7 @@ endpoint "/hello" {
 		},
 	}
 	require.EqualValues(t, expected, test.Endpoints[0])
-}
-
-func TestReEncode(t *testing.T) {
-	testCases := []struct {
-		Name, Body string
-	}{
-		{
-			Name: "attr",
-			Body: `year = 2021`,
-		},
-		{
-			Name: "simple resource",
-			Body: `project "atlas" {
-	started_year = 2021
-	useful = true
-}`,
-		},
-		{
-			Name: "nested resource",
-			Body: `author "rotemtam" {
-	package "schemahcl" {
-		lang = "go"
-	}
-}`,
-		},
-		{
-			Name: "block reference",
-			Body: `user "rotemtam" {
-}
-task "code" {
-	owner = user.rotemtam
-}
-`,
-		},
-	}
-	for _, tt := range testCases {
-		t.Run(tt.Name, func(t *testing.T) {
-			resource, err := decode([]byte(tt.Body))
-			require.NoError(t, err)
-			bytes, err := Encode(resource)
-			require.NoError(t, err)
-			again, err := decode(bytes)
-			require.NoError(t, err)
-			require.EqualValues(t, resource, again, "expected resource to be the same after encoding")
-		})
-	}
+	buf, err := Marshal(&test)
+	require.NoError(t, err)
+	require.EqualValues(t, f, string(buf))
 }