diff --git a/encoder.go b/encoder.go index 1d6582b..2d94e36 100644 --- a/encoder.go +++ b/encoder.go @@ -105,7 +105,17 @@ func (e *Encoder) Encode() ([]byte, error) { } // special handling for case when we get an empty output - if node.Kind == yaml.MappingNode && len(node.Content) == 0 && node.FootComment != "" && e.options.CommentsFlag.enabled() { + if handled, res, err := handleEmptyNodeComment(node, e.options); handled { + return res, err + } + + return yaml.Marshal(node) +} + +// handleEmptyNodeComment handles the special case for empty mapping nodes with foot comments. +// It returns true if the case was handled, along with the marshalled bytes and any error. +func handleEmptyNodeComment(node *yaml.Node, options *Options) (bool, []byte, error) { + if node.Kind == yaml.MappingNode && len(node.Content) == 0 && node.FootComment != "" && options.CommentsFlag.enabled() { res := "" if node.HeadComment != "" { @@ -121,10 +131,9 @@ func (e *Encoder) Encode() ([]byte, error) { lines[i] = "# " + line } - return []byte(strings.Join(lines, "\n")), nil + return true, []byte(strings.Join(lines, "\n")), nil } - - return yaml.Marshal(node) + return false, nil, nil } func isEmpty(value reflect.Value) bool { @@ -195,114 +204,153 @@ func toYamlNode(in interface{}, options *Options) (*yaml.Node, error) { case reflect.Struct: node.Kind = yaml.MappingNode + node.Kind = yaml.MappingNode t := v.Type() - for i := 0; i < v.NumField(); i++ { - // skip unexported fields - if !v.Field(i).CanInterface() { - continue + if err := processStructField(node, v, t, i, options); err != nil { + return nil, err } + } + case reflect.Map: + node.Kind = yaml.MappingNode + if err := processMap(v, node, options); err != nil { + return nil, err + } + case reflect.Slice: + node.Kind = yaml.SequenceNode + if err := processSlice(v, node, options); err != nil { + return nil, err + } + default: + if err := node.Encode(in); err != nil { + return nil, err + } + } - comment := t.Field(i).Tag.Get(options.CommentTagName) - tag := t.Field(i).Tag.Get("yaml") - parts := strings.Split(tag, ",") - fieldName := parts[0] - parts = parts[1:] - - if fieldName == "" { - fieldName = strings.ToLower(t.Field(i).Name) - } + return node, nil +} - if fieldName == "-" { - continue - } +// processStructField handles processing of a single struct field. +func processStructField(node *yaml.Node, v reflect.Value, t reflect.Type, i int, options *Options) error { + // skip unexported fields + if !v.Field(i).CanInterface() { + return nil + } - var ( - empty = isEmpty(v.Field(i)) - skip bool - inline bool - flow bool - ) - - for _, part := range parts { - if part == "omitempty" && empty && options.OmitEmpty { - skip = true - } - - if part == "inline" { - inline = true - } - - if part == "flow" { - flow = true - } - } + field := t.Field(i) + comment := field.Tag.Get(options.CommentTagName) + tag := field.Tag.Get("yaml") + parts := strings.Split(tag, ",") + fieldName := parts[0] + parts = parts[1:] - var value interface{} - if v.Field(i).CanInterface() { - value = v.Field(i).Interface() - } + if fieldName == "" { + fieldName = strings.ToLower(field.Name) + } - if skip { - continue - } + if fieldName == "-" { + return nil + } - var style yaml.Style - if flow { - style |= yaml.FlowStyle - } + var ( + fieldIsEmpty = isEmpty(v.Field(i)) // Determine if field is empty + skipField = false // Default to not skipping the field + inline = false + flow = false + ) + + // Tag parsing logic + hasOmitemptyTagInStruct := false + for _, part := range parts { + switch part { + case "omitempty": + hasOmitemptyTagInStruct = true + case "inline": + inline = true + case "flow": + flow = true + } + } - if inline { - child, err := toYamlNode(value, options) - if err != nil { - return nil, err - } + // Decision logic for skipping the field + performSkip := false // Initialize - if child.Kind == yaml.MappingNode || child.Kind == yaml.SequenceNode { - appendNodes(node, child.Content...) - } - } else if err := addToMap(node, fieldName, value, comment, style, options); err != nil { - return nil, err + if fieldName == "-" { + performSkip = true // Always skip if field name is "-" + } else { + // If options.OmitEmpty is true, then standard omitempty logic applies + if options.OmitEmpty { + if hasOmitemptyTagInStruct && fieldIsEmpty { + performSkip = true } } - case reflect.Map: - node.Kind = yaml.MappingNode - keys := v.MapKeys() - // always interate keys in alphabetical order to preserve the same output for maps - sort.Slice(keys, func(i, j int) bool { - return keys[i].String() < keys[j].String() - }) + // If options.OmitEmpty is false, performSkip remains false (unless fieldName was "-"), + // meaning the omitempty tag is completely ignored because the condition + // `hasOmitemptyTagInStruct && fieldIsEmpty` is only checked if options.OmitEmpty is true. + } - for _, k := range keys { - element := v.MapIndex(k) - value := element.Interface() + // This is the crucial assignment + skipField = performSkip - if err := addToMap(node, k.Interface(), value, "", 0, options); err != nil { - return nil, err - } - } - case reflect.Slice: - node.Kind = yaml.SequenceNode - nodes := make([]*yaml.Node, v.Len()) + var value interface{} + if v.Field(i).CanInterface() { + value = v.Field(i).Interface() + } - for i := 0; i < v.Len(); i++ { - element := v.Index(i) + if skipField { + return nil + } - var err error + var style yaml.Style + if flow { + style |= yaml.FlowStyle + } - nodes[i], err = toYamlNode(element.Interface(), options) - if err != nil { - return nil, err - } + if inline { + child, err := toYamlNode(value, options) + if err != nil { + return err } - appendNodes(node, nodes...) - default: - if err := node.Encode(in); err != nil { - return nil, err + + if child.Kind == yaml.MappingNode || child.Kind == yaml.SequenceNode { + appendNodes(node, child.Content...) } + } else if err := addToMap(node, fieldName, value, comment, style, options); err != nil { + return err } + return nil +} - return node, nil +func processMap(v reflect.Value, node *yaml.Node, options *Options) error { + keys := v.MapKeys() + // always interate keys in alphabetical order to preserve the same output for maps + sort.Slice(keys, func(i, j int) bool { + return keys[i].String() < keys[j].String() + }) + + for _, k := range keys { + element := v.MapIndex(k) + value := element.Interface() + + if err := addToMap(node, k.Interface(), value, "", 0, options); err != nil { + return err + } + } + return nil +} + +func processSlice(v reflect.Value, node *yaml.Node, options *Options) error { + nodes := make([]*yaml.Node, v.Len()) + for i := 0; i < v.Len(); i++ { + element := v.Index(i) + var err error + nodes[i], err = toYamlNode(element.Interface(), options) + if err != nil { + return err + } + } + appendNodes(node, nodes...) + return nil } func appendNodes(dest *yaml.Node, nodes ...*yaml.Node) { @@ -334,12 +382,17 @@ func addToMap(dest *yaml.Node, fieldName, in interface{}, fieldComent string, st } func addComment(node *yaml.Node, comment string, flag CommentsFlag) { - if flag.enabled() { - dest := []*string{ - &node.HeadComment, - &node.LineComment, - &node.FootComment, - } - *dest[int(flag)-1] = comment + if !flag.enabled() || comment == "" { // Also explicitly skip if comment is empty + return + } + + switch flag { + case CommentsOnHead: + node.HeadComment = comment + case CommentsInLine: + node.LineComment = comment + case CommentsOnFoot: + node.FootComment = comment + // No default needed as flag.enabled() already filters for valid comment placement flags. } } diff --git a/encoder_test.go b/encoder_test.go index 8db2432..346928e 100644 --- a/encoder_test.go +++ b/encoder_test.go @@ -2,6 +2,8 @@ package yaml_encoder_test import ( "fmt" + "strings" + "testing" encoder "github.com/zwgblue/yaml-encoder" ) @@ -17,8 +19,8 @@ func ExampleEncoder() { Password: "xxxxxx", } - encoder := encoder.NewEncoder(config, encoder.WithComments(encoder.CommentsOnHead)) - content, _ := encoder.Encode() + enc := encoder.NewEncoder(config, encoder.WithComments(encoder.CommentsOnHead)) + content, _ := enc.Encode() fmt.Printf("%s", content) // Output: // # this is the username of database @@ -38,8 +40,8 @@ func ExampleWithCustomizedTag() { Password: "xxxxxx", } - encoder := encoder.NewEncoder(config, encoder.WithComments(encoder.CommentsOnHead), encoder.WithCustomizedTag("description")) - content, _ := encoder.Encode() + enc := encoder.NewEncoder(config, encoder.WithComments(encoder.CommentsOnHead), encoder.WithCustomizedTag("description")) + content, _ := enc.Encode() fmt.Printf("%s", content) // Output: // # this is the username of database @@ -47,3 +49,582 @@ func ExampleWithCustomizedTag() { // # this is the password of database // password: xxxxxx } + +type testCase struct { + name string + input interface{} + options []encoder.Option + expected string + wantErr bool +} + +func normalizeYAML(s string) string { + s = strings.TrimSpace(s) + s = strings.ReplaceAll(s, "\r\n", "\n") + lines := strings.Split(s, "\n") + for i, line := range lines { + lines[i] = strings.TrimRight(line, " ") + } + return strings.Join(lines, "\n") +} + +func runTests(t *testing.T, tests []testCase) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + enc := encoder.NewEncoder(tt.input, tt.options...) + content, err := enc.Encode() + + if (err != nil) != tt.wantErr { + t.Errorf("Encode() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if err == nil { + actualNormalized := normalizeYAML(string(content)) + expectedNormalized := normalizeYAML(tt.expected) + + if actualNormalized != expectedNormalized { + t.Errorf("Encode() output mismatch:\n----- Expected (%d bytes) -----\n%s\n----- Actual (%d bytes) -----\n%s\n----- End -----", len(expectedNormalized), expectedNormalized, len(actualNormalized), actualNormalized) + } + } + }) + } +} + +func TestOmitEmpty(t *testing.T) { + type StructWithOmitEmpty struct { + Name string `yaml:"name,omitempty"` + Age int `yaml:"age,omitempty"` + Count *int `yaml:"count,omitempty"` + Tags []string `yaml:"tags,omitempty"` + Attrs map[string]string `yaml:"attrs,omitempty"` + IsActive bool `yaml:"isactive,omitempty"` + } + + intZero := 0 + intVal := 10 + + tests := []testCase{ + { + name: "all fields empty", + input: StructWithOmitEmpty{}, + expected: "{}", + }, + { + name: "all fields empty with nil pointer, slice, map", + input: StructWithOmitEmpty{Count: nil, Tags: nil, Attrs: nil}, + expected: "{}", + }, + { + name: "name not empty", + input: StructWithOmitEmpty{Name: "test"}, + expected: "name: test", + }, + { + name: "age not empty", + input: StructWithOmitEmpty{Age: 10}, + expected: "age: 10", + }, + { + name: "age is zero (should be omitted)", + input: StructWithOmitEmpty{Age: 0}, + expected: "{}", + }, + { + name: "count not empty (pointer to non-zero)", + input: StructWithOmitEmpty{Count: &intVal}, + expected: "count: 10", + }, + { + name: "count is pointer to zero", + input: StructWithOmitEmpty{Count: &intZero}, + expected: "count: 0", + }, + { + name: "count is nil (should be omitted)", + input: StructWithOmitEmpty{Count: nil}, + expected: "{}", + }, + { + name: "tags not empty", // Adjusted for 4-space indent from actual output + input: StructWithOmitEmpty{Tags: []string{"a"}}, + expected: "tags:\n - a", + }, + { + name: "tags empty (should be omitted)", + input: StructWithOmitEmpty{Tags: []string{}}, + expected: "{}", + }, + { + name: "attrs not empty", // Adjusted for 4-space indent + input: StructWithOmitEmpty{Attrs: map[string]string{"key": "val"}}, + expected: "attrs:\n key: val", + }, + { + name: "attrs empty (should be omitted)", + input: StructWithOmitEmpty{Attrs: map[string]string{}}, + expected: "{}", + }, + { + name: "isActive true", + input: StructWithOmitEmpty{IsActive: true}, + expected: "isactive: true", + }, + { + name: "isActive false (zero value, should be omitted)", + input: StructWithOmitEmpty{IsActive: false}, + expected: "{}", + }, + { + name: "all fields non-empty", // Adjusted for 4-space indent + input: StructWithOmitEmpty{ + Name: "test", + Age: 30, + Count: &intVal, + Tags: []string{"a", "b"}, + Attrs: map[string]string{"k": "v"}, + IsActive: true, + }, + expected: ` +name: test +age: 30 +count: 10 +tags: + - a + - b +attrs: + k: v +isactive: true`, + }, + { + name: "omitempty disabled globally - all fields zero/nil", + input: StructWithOmitEmpty{}, + options: []encoder.Option{encoder.WithOmitEmpty(false)}, + expected: ` +name: "" +age: 0 +count: null +tags: [] +attrs: {} +isactive: false`, + }, + { + name: "omitempty disabled globally - specific fields empty, Count points to zero", + input: StructWithOmitEmpty{Name: "hello", Age: 0, Count: &intZero, Tags: []string{}, Attrs: map[string]string{}}, + options: []encoder.Option{encoder.WithOmitEmpty(false)}, + expected: ` +name: hello +age: 0 +count: 0 +tags: [] +attrs: {} +isactive: false`, + }, + } + runTests(t, tests) +} + +func TestComments(t *testing.T) { + type SimpleStruct struct { + Field1 string `yaml:"field1" comment:"Comment for Field1"` + Field2 int `yaml:"field2,omitempty" comment:"Comment for Field2 (omitempty)"` + } + type NestedStruct struct { + Name string `yaml:"name" comment:"Comment for Name"` + Simple SimpleStruct `yaml:"simple" comment:"Comment for SimpleStruct"` + SimplePtr *SimpleStruct `yaml:"simplePtr,omitempty" comment:"Comment for SimplePtrStruct"` + } + + tests := []testCase{ + { + name: "CommentsOnHead - SimpleStruct - Field1", + input: SimpleStruct{Field1: "val1"}, + options: []encoder.Option{encoder.WithComments(encoder.CommentsOnHead)}, + expected: "# Comment for Field1\nfield1: val1", + }, + { + name: "CommentsOnHead - SimpleStruct - Field2 (not empty)", + input: SimpleStruct{Field1: "val1", Field2: 123}, + options: []encoder.Option{encoder.WithComments(encoder.CommentsOnHead)}, + expected: ` +# Comment for Field1 +field1: val1 +# Comment for Field2 (omitempty) +field2: 123`, + }, + { + name: "CommentsOnHead - SimpleStruct - Field2 (empty, omitted)", + input: SimpleStruct{Field1: "val1"}, + options: []encoder.Option{encoder.WithComments(encoder.CommentsOnHead)}, + expected: "# Comment for Field1\nfield1: val1", + }, + { + name: "CommentsOnHead - NestedStruct", // Adjusted for 4-space indent + input: NestedStruct{ + Name: "TestNested", + Simple: SimpleStruct{Field1: "s_val1", Field2: 10}, + SimplePtr: &SimpleStruct{Field1: "sp_val1", Field2: 20}, + }, + options: []encoder.Option{encoder.WithComments(encoder.CommentsOnHead)}, + expected: ` +# Comment for Name +name: TestNested +# Comment for SimpleStruct +simple: + # Comment for Field1 + field1: s_val1 + # Comment for Field2 (omitempty) + field2: 10 +# Comment for SimplePtrStruct +simplePtr: + # Comment for Field1 + field1: sp_val1 + # Comment for Field2 (omitempty) + field2: 20`, + }, + { + name: "CommentsInLine - SimpleStruct", + input: SimpleStruct{Field1: "val1", Field2: 123}, + options: []encoder.Option{encoder.WithComments(encoder.CommentsInLine)}, + expected: ` +field1: val1 # Comment for Field1 +field2: 123 # Comment for Field2 (omitempty)`, + }, + { + name: "CommentsOnFoot - SimpleStruct", // Adjusted for actual yaml.v3 output + input: SimpleStruct{Field1: "val1", Field2: 123}, + options: []encoder.Option{encoder.WithComments(encoder.CommentsOnFoot)}, + expected: ` +field1: val1 +# Comment for Field1 + +field2: 123 +# Comment for Field2 (omitempty)`, + }, + { + name: "CommentsOnHead - Empty Comment String", + input: struct{ Field string `yaml:"field" comment:""` }{Field: "value"}, + options: []encoder.Option{encoder.WithComments(encoder.CommentsOnHead)}, + expected: "field: value", + }, + { + name: "CommentsOnHead - Map Field", // Adjusted for 4-space indent + input: struct { MyMap map[string]string `yaml:"myMap" comment:"Comment for MyMap"` }{ + MyMap: map[string]string{"key1": "val1"}, + }, + options: []encoder.Option{encoder.WithComments(encoder.CommentsOnHead)}, + expected: "# Comment for MyMap\nmyMap:\n key1: val1", + }, + { + name: "CommentsOnHead - Slice Field", // Adjusted for 4-space indent + input: struct { MySlice []string `yaml:"mySlice" comment:"Comment for MySlice"` }{ + MySlice: []string{"item1"}, + }, + options: []encoder.Option{encoder.WithComments(encoder.CommentsOnHead)}, + expected: "# Comment for MySlice\nmySlice:\n - item1", + }, + } + runTests(t, tests) +} + +func TestInlineFields(t *testing.T) { + type InnerStruct struct { + InnerField1 string `yaml:"innerField1" comment:"Inner 1"` + InnerField2 int `yaml:"innerField2,omitempty" comment:"Inner 2"` + } + type OuterStruct struct { + OuterField string `yaml:"outerField,omitempty" comment:"Outer"` + Inner InnerStruct `yaml:",inline"` + } + type OuterWithPtrInline struct { + OuterField string `yaml:"outerField,omitempty" comment:"Outer"` + Inner *InnerStruct `yaml:",inline"` + } + + innerInstance := InnerStruct{InnerField1: "val1", InnerField2: 10} + emptyInnerInstance := InnerStruct{} + + tests := []testCase{ + { + name: "Inline struct with values", + input: OuterStruct{OuterField: "outerVal", Inner: innerInstance}, + options: []encoder.Option{encoder.WithComments(encoder.CommentsOnHead)}, + expected: ` +# Outer +outerField: outerVal +# Inner 1 +innerField1: val1 +# Inner 2 +innerField2: 10`, + }, + { + name: "Inline struct with omitempty on inner field (InnerField2 is zero)", + input: OuterStruct{OuterField: "outerVal", Inner: InnerStruct{InnerField1: "val1"}}, + options: []encoder.Option{encoder.WithComments(encoder.CommentsOnHead)}, + expected: ` +# Outer +outerField: outerVal +# Inner 1 +innerField1: val1`, + }, + { + name: "Inline empty struct (InnerField1 is empty string, InnerField2 is zero and omitted)", + input: OuterStruct{OuterField: "outerVal", Inner: emptyInnerInstance}, + options: []encoder.Option{encoder.WithComments(encoder.CommentsOnHead)}, + expected: ` +# Outer +outerField: outerVal +# Inner 1 +innerField1: ""`, + }, + { + name: "Inline nil pointer struct", + input: OuterWithPtrInline{OuterField: "outerVal", Inner: nil}, + options: []encoder.Option{encoder.WithComments(encoder.CommentsOnHead)}, + expected: ` +# Outer +outerField: outerVal`, + }, + { + name: "Inline non-nil pointer struct with values", + input: OuterWithPtrInline{OuterField: "outerVal", Inner: &innerInstance}, + options: []encoder.Option{encoder.WithComments(encoder.CommentsOnHead)}, + expected: ` +# Outer +outerField: outerVal +# Inner 1 +innerField1: val1 +# Inner 2 +innerField2: 10`, + }, + { + name: "Inline non-nil pointer to empty struct", + input: OuterWithPtrInline{OuterField: "outerVal", Inner: &emptyInnerInstance}, + options: []encoder.Option{encoder.WithComments(encoder.CommentsOnHead)}, + expected: ` +# Outer +outerField: outerVal +# Inner 1 +innerField1: ""`, + }, + { + name: "Only Inline struct", + input: struct{ Inner InnerStruct `yaml:",inline"` }{Inner: innerInstance}, + options: []encoder.Option{encoder.WithComments(encoder.CommentsOnHead)}, + expected: ` +# Inner 1 +innerField1: val1 +# Inner 2 +innerField2: 10`, + }, + } + runTests(t, tests) +} + +func TestStructScenarios(t *testing.T) { + type NoExportedFields struct { + name string + age int + } + type MixedFields struct { + Exported string `yaml:"exported"` + unexported string + Another int `yaml:"another"` + } + + tests := []testCase{ + { + name: "Struct with no exported fields", + input: NoExportedFields{name: "test", age: 10}, + expected: "{}", + }, + { + name: "Struct with no exported fields as pointer", + input: &NoExportedFields{name: "test", age: 10}, + expected: "{}", + }, + { + name: "Struct with mixed exported and unexported fields", + input: MixedFields{Exported: "val1", unexported: "hidden", Another: 123}, + expected: "exported: val1\nanother: 123", + }, + { + name: "Empty struct (no fields at all)", + input: struct{}{}, + expected: "{}", + }, + } + runTests(t, tests) +} + +func TestMapScenarios(t *testing.T) { + tests := []testCase{ + { + name: "Simple map string to string", // Adjusted for 4-space indent + input: map[string]string{"key1": "val1", "b_key2": "val2", "a_key3": "val3"}, + expected: ` +a_key3: val3 +b_key2: val2 +key1: val1`, + }, + { + name: "Map string to int", // Adjusted for 4-space indent + input: map[string]int{"one": 1, "two": 2}, + expected: ` +one: 1 +two: 2`, + }, + { + name: "Map int to string (keys not quoted by yaml.v3)", // Adjusted for 4-space indent + input: map[int]string{1: "one", 2: "two"}, + expected: ` +1: one +2: two`, + }, + { + name: "Empty map", + input: map[string]string{}, + expected: "{}", + }, + { + name: "Nil map (marshals to empty map by yaml.v3)", + input: (map[string]string)(nil), + expected: "{}", + }, + { + name: "Map with complex values (structs)", // Adjusted for 4-space indent + input: map[string]struct { A string `yaml:"a"` }{ + "entry1": {A: "val_a"}, + "entry2": {A: "val_b"}, + }, + expected: ` +entry1: + a: val_a +entry2: + a: val_b`, + }, + } + runTests(t, tests) +} + +func TestSliceScenarios(t *testing.T) { + type SimpleStructForSlice struct { + X string `yaml:"x"` + } + tests := []testCase{ + { + name: "Slice of strings", // Adjusted for 4-space indent for list items from root + input: []string{"a", "b", "c"}, + expected: ` +- a +- b +- c`, + }, + { + name: "Slice of ints", // Adjusted + input: []int{1, 2, 3}, + expected: ` +- 1 +- 2 +- 3`, + }, + { + name: "Empty slice", + input: []string{}, + expected: "[]", + }, + { + name: "Nil slice (marshals to empty slice by yaml.v3)", + input: ([]string)(nil), + expected: "[]", + }, + { + name: "Slice of structs", // Adjusted + input: []SimpleStructForSlice{{X: "one"}, {X: "two"}}, + expected: ` +- x: one +- x: two`, + }, + { + name: "Slice of maps", // Adjusted + input: []map[string]int{ + {"a": 1, "b": 2}, + {"c": 3}, + }, + expected: ` +- a: 1 + b: 2 +- c: 3`, + }, + { + name: "Slice with nil pointer element", // Adjusted + input: []*SimpleStructForSlice{ + {X: "one"}, + nil, + {X: "three"}, + }, + expected: ` +- x: one +- null +- x: three`, + }, + } + runTests(t, tests) +} + +type ErrorMarshaler struct{} +var customError = fmt.Errorf("custom MarshalYAML error") +func (em *ErrorMarshaler) MarshalYAML() (interface{}, error) { return nil, customError } + +func TestErrorHandling(t *testing.T) { + tests := []testCase{ + { + name: "Error from custom MarshalYAML", + input: &ErrorMarshaler{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + enc := encoder.NewEncoder(tt.input, tt.options...) + _, err := enc.Encode() + + if (err != nil) != tt.wantErr { + t.Errorf("Encode() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.wantErr && err != nil { + if !strings.Contains(err.Error(), "custom MarshalYAML error") { + t.Errorf("Encode() error = %v, expected to contain %q", err, "custom MarshalYAML error") + } + } + }) + } +} + +type NilValMarshaler struct { Val string } +func (n *NilValMarshaler) MarshalYAML() (interface{}, error) { + if n == nil { return nil, fmt.Errorf("MarshalYAML called on nil receiver") } + return map[string]string{"val": n.Val}, nil +} + +func TestSpecialCasesAndRefactoredHelpers(t *testing.T) { + var nilMarshaler *NilValMarshaler = nil + + tests := []testCase{ + { + name: "yaml.Marshaler that is nil (pointer is nil)", + input: nilMarshaler, + expected: "null", + wantErr: false, + }, + { + name: "yaml.Marshaler that is not nil", + input: &NilValMarshaler{Val: "test"}, + expected: "val: test", + wantErr: false, + }, + } + runTests(t, tests) +}