Repository: amzn/ion-go
Pull request #104 (2020-07-25 06:33:53)

Add annotations when Marshalling values
Encoder does not take annotations and consequently Marshal cannot serialize values with annotations.

To get the annotations when Marshalling, user should send a struct to Marshal() whereby one field of the struct is a slice of string and tagged as `ion:",annotations"`.

For instance:
```
type foo struct {
   Value   int
   AnyName []string `ion:",annotations"`
}
...
val := foo{5, []string{"age"}}
buf := MarshalText(val)
```


Patch:
diff --git a/ion/fields.go b/ion/fields.go
index 40cf231..21e6282 100644
--- a/ion/fields.go
+++ b/ion/fields.go
@@ -8,11 +8,12 @@ import (
 
 // A field is a reflectively-accessed field of a struct type.
 type field struct {
-	name      string
-	typ       reflect.Type
-	path      []int
-	omitEmpty bool
-	hint      Type
+	name        string
+	typ         reflect.Type
+	path        []int
+	omitEmpty   bool
+	hint        Type
+	annotations bool
 }
 
 func (f *field) setopts(opts string) {
@@ -35,6 +36,8 @@ func (f *field) setopts(opts string) {
 			f.hint = ClobType
 		case "sexp":
 			f.hint = SexpType
+		case "annotations":
+			f.annotations = true
 		}
 	}
 }
diff --git a/ion/marshal.go b/ion/marshal.go
index aa6d949..28551c1 100644
--- a/ion/marshal.go
+++ b/ion/marshal.go
@@ -179,7 +179,7 @@ func (m *Encoder) encodeValue(v reflect.Value, hint Type) error {
 		return m.encodePtr(v, hint)
 
 	case reflect.Struct:
-		return m.encodeStruct(v, hint)
+		return m.encodeStruct(v)
 
 	case reflect.Map:
 		return m.encodeMap(v, hint)
@@ -299,7 +299,14 @@ func (m *Encoder) encodeArray(v reflect.Value, hint Type) error {
 }
 
 // EncodeStruct encodes a struct to the output writer as an Ion struct.
-func (m *Encoder) encodeStruct(v reflect.Value, hint Type) error {
+func (m *Encoder) encodeStruct(v reflect.Value) error {
+	fields := fieldsFor(v.Type())
+	for _, field := range fields {
+		if field.annotations {
+			return m.encodeWithAnnotation(v)
+		}
+	}
+
 	t := v.Type()
 	if t == timeType {
 		return m.encodeTime(v)
@@ -308,9 +315,9 @@ func (m *Encoder) encodeStruct(v reflect.Value, hint Type) error {
 		return m.encodeDecimal(v)
 	}
 
-	fields := fieldsFor(v.Type())
-
-	m.w.BeginStruct()
+	if err := m.w.BeginStruct(); err != nil {
+		return err
+	}
 
 FieldLoop:
 	for i := range fields {
@@ -352,6 +359,31 @@ func (m *Encoder) encodeDecimal(v reflect.Value) error {
 	return m.w.WriteDecimal(d)
 }
 
+func (m *Encoder) encodeWithAnnotation(v reflect.Value) error {
+	original := v
+	fields := fieldsFor(v.Type())
+	for _, field := range fields {
+		if field.annotations {
+			annotations, err := findSubvalue(original, &field)
+			if err != nil {
+				return err
+			}
+			if annotations.Kind() != reflect.Slice {
+				return fmt.Errorf("ion: '%v' is provided for annotations,"+
+					"it must be of type []string", annotations.Kind())
+			}
+			listOfAnnotations := annotations.Interface().([]string)
+			err = m.w.Annotations(listOfAnnotations...)
+			if err != nil {
+				return err
+			}
+		} else {
+			v, _ = findSubvalue(original, &field)
+		}
+	}
+	return m.encodeValue(v, NoType)
+}
+
 // EmptyValue returns true if the given value is the empty value for its type.
 func emptyValue(v reflect.Value) bool {
 	switch v.Kind() {


Test patch:
diff --git a/ion/marshal_test.go b/ion/marshal_test.go
index 89fcfd6..2012753 100644
--- a/ion/marshal_test.go
+++ b/ion/marshal_test.go
@@ -285,3 +285,38 @@ func TestMarshalCustomMarshaler(t *testing.T) {
 		t.Errorf("expected %v, got %v", eval, val)
 	}
 }
+
+func TestMarshalValuesWithAnnotation(t *testing.T) {
+	test := func(v interface{}, testName, eval string) {
+		t.Run(testName, func(t *testing.T) {
+			val, err := MarshalText(v)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if string(val) != eval {
+				t.Errorf("expected '%v', got '%v'", eval, string(val))
+			}
+		})
+	}
+
+	type foo struct {
+		Value   interface{}
+		AnyName []string `ion:",annotations"`
+	}
+
+	buildValue := func(val interface{}) foo {
+		return foo{val, []string{"symbols or string", "annotations"}}
+	}
+
+	test(buildValue(nil), "null", "'symbols or string'::annotations::null")
+	test(buildValue(true), "bool", "'symbols or string'::annotations::true")
+	test(buildValue(5), "int", "'symbols or string'::annotations::5")
+	test(buildValue(float32(math.MaxFloat32)), "float", "'symbols or string'::annotations::3.4028234663852886e+38")
+	test(buildValue(MustParseDecimal("1.2")), "decimal", "'symbols or string'::annotations::1.2")
+	test(buildValue(time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC)),
+		"timestamp", "'symbols or string'::annotations::2000-01-02T03:04:05Z")
+	test(buildValue("stringValue"), "string", "'symbols or string'::annotations::\"stringValue\"")
+	test(buildValue([]byte{4, 2}), "blob", "'symbols or string'::annotations::{{BAI=}}")
+	test(buildValue([]int{3, 5, 7}), "list", "'symbols or string'::annotations::[3,5,7]")
+	test(buildValue(map[string]int{"b": 2, "a": 1}), "struct", "'symbols or string'::annotations::{a:1,b:2}")
+}