Files
go-render/render_test.go
Jim Myhrberg 66560625d6 feat(deps): replace yaml.v3 with goccy/go-yaml library
Replace yaml.v3 with goccy/go-yaml, as the former is now unmaintained.

Also upgrade minimum Go version to 1.21.0 and update testify to v1.10.0.

Add support for configuring encoder options in YAML renderer to
provide more flexibility in YAML output formatting. Include new options
for sequence indentation and automatic integer conversion.

Implement support for both yaml.InterfaceMarshaler and 
yaml.BytesMarshaler interfaces with appropriate test cases. Rename mock
implementation to clarify interface implementation.
2025-06-22 11:49:56 +01:00

774 lines
18 KiB
Go

package render
import (
"bytes"
"encoding/xml"
"errors"
"io"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
// mockWriter is a mock implementation of io.Writer.
type mockWriter struct {
WriteErr error
buf bytes.Buffer
}
var _ io.Writer = (*mockWriter)(nil)
func (mw *mockWriter) Write(p []byte) (n int, err error) {
if mw.WriteErr != nil {
return 0, mw.WriteErr
}
return mw.buf.Write(p)
}
func (mw *mockWriter) String() string {
return mw.buf.String()
}
// mockHandler is a mock implementation of Handler.
type mockHandler struct {
output string
formats []string
err error
}
var (
_ Handler = (*mockHandler)(nil)
_ FormatsHandler = (*mockHandler)(nil)
)
func (mh *mockHandler) Render(w io.Writer, _ any) error {
_, err := w.Write([]byte(mh.output))
if mh.err != nil {
return mh.err
}
return err
}
func (mh *mockHandler) Formats() []string {
return mh.formats
}
// mockPrettyHandler is a mock implementation of PrettyHandler.
type mockPrettyHandler struct {
output string
prettyOutput string
formats []string
err error
}
var (
_ Handler = (*mockPrettyHandler)(nil)
_ PrettyHandler = (*mockPrettyHandler)(nil)
_ FormatsHandler = (*mockPrettyHandler)(nil)
)
func (mph *mockPrettyHandler) Render(w io.Writer, _ any) error {
_, err := w.Write([]byte(mph.output))
if mph.err != nil {
return mph.err
}
return err
}
func (mph *mockPrettyHandler) RenderPretty(w io.Writer, _ any) error {
_, err := w.Write([]byte(mph.prettyOutput))
if mph.err != nil {
return mph.err
}
return err
}
func (mph *mockPrettyHandler) Formats() []string {
return mph.formats
}
// mockFormatsHandler is a mock implementation of FormatsHandler.
type mockFormatsHandler struct {
output string
formats []string
err error
}
var (
_ Handler = (*mockFormatsHandler)(nil)
_ FormatsHandler = (*mockFormatsHandler)(nil)
)
func (mph *mockFormatsHandler) Render(w io.Writer, _ any) error {
_, err := w.Write([]byte(mph.output))
if mph.err != nil {
return mph.err
}
return err
}
func (mph *mockFormatsHandler) Formats() []string {
return mph.formats
}
type renderFormatTestCase struct {
name string
writeErr error
formats []string
value any
valueFunc func() any
want string
wantPretty string
wantCompact string
wantErr string
wantErrIs []error
}
// "binary" format.
var binaryFormatTestCases = []renderFormatTestCase{
{
name: "with binary marshaler",
formats: []string{"binary", "bin"},
value: &mockBinaryMarshaler{data: []byte("test string")},
want: "test string",
},
{
name: "capitalized format",
formats: []string{"BINARY", "BIN"},
value: &mockBinaryMarshaler{data: []byte("test string")},
want: "test string",
},
{
name: "without binary marshaler",
formats: []string{"binary", "bin"},
value: struct{}{},
wantErr: "render: unsupported format: {{format}}",
wantErrIs: []error{Err, ErrUnsupportedFormat},
},
{
name: "with error marshaling",
formats: []string{"binary", "bin"},
value: &mockBinaryMarshaler{
data: []byte("test string"),
err: errors.New("marshal error!!1"),
},
wantErr: "render: failed: marshal error!!1",
wantErrIs: []error{Err, ErrFailed},
},
{
name: "with error writing to writer",
formats: []string{"binary", "bin"},
writeErr: errors.New("write error!!1"),
value: &mockBinaryMarshaler{data: []byte("test string")},
wantErr: "render: failed: write error!!1",
wantErrIs: []error{Err, ErrFailed},
},
{
name: "with invalid type",
formats: []string{"binary", "bin"},
value: make(chan int),
wantErr: "render: unsupported format: {{format}}",
wantErrIs: []error{Err, ErrUnsupportedFormat},
},
}
// "json" format.
var jsonFormatTestCases = []renderFormatTestCase{
{
name: "with map",
formats: []string{"json"},
value: map[string]int{"age": 30},
wantPretty: "{\n \"age\": 30\n}\n",
wantCompact: "{\"age\":30}\n",
},
{
name: "capitalized format",
formats: []string{"JSON"},
value: map[string]int{"age": 30},
wantPretty: "{\n \"age\": 30\n}\n",
wantCompact: "{\"age\":30}\n",
},
{
name: "with json marshaler",
formats: []string{"json"},
value: &mockJSONMarshaler{data: []byte(`{"age":30}`)},
wantPretty: "{\n \"age\": 30\n}\n",
wantCompact: "{\"age\":30}\n",
},
{
name: "with error from json marshaler",
formats: []string{"json"},
value: &mockJSONMarshaler{err: errors.New("marshal error!!1")},
wantErrIs: []error{Err},
},
{
name: "with error writing to writer",
formats: []string{"json"},
writeErr: errors.New("write error!!1"),
value: map[string]int{"age": 30},
wantErr: "render: failed: write error!!1",
wantErrIs: []error{Err, ErrFailed},
},
{
name: "with invalid type",
formats: []string{"json"},
value: make(chan int),
wantErr: "render: failed: json: unsupported type: chan int",
wantErrIs: []error{Err, ErrFailed},
},
}
// "text" format.
var textFormatTestCases = []renderFormatTestCase{
{
name: "nil",
formats: []string{"text", "txt", "plain"},
value: nil,
wantErr: "render: unsupported format: {{format}}",
wantErrIs: []error{Err, ErrUnsupportedFormat},
},
{
name: "capitalized format",
formats: []string{"TEXT", "TXT", "PLAIN"},
value: []byte("test byte slice"),
want: "test byte slice",
},
{
name: "byte slice",
formats: []string{"text", "txt", "plain"},
value: []byte("test byte slice"),
want: "test byte slice",
},
{
name: "nil byte slice",
formats: []string{"text", "txt", "plain"},
value: []byte(nil),
want: "",
},
{
name: "empty byte slice",
formats: []string{"text", "txt", "plain"},
value: []byte{},
want: "",
},
{
name: "rune slice",
formats: []string{"text", "txt", "plain"},
value: []rune{'r', 'u', 'n', 'e', 's', '!', ' ', 'y', 'e', 's'},
want: "runes! yes",
},
{
name: "string",
formats: []string{"text", "txt", "plain"},
value: "test string",
want: "test string",
},
{
name: "int",
formats: []string{"text", "txt", "plain"},
value: int(42),
want: "42",
},
{
name: "int8",
formats: []string{"text", "txt", "plain"},
value: int8(43),
want: "43",
},
{
name: "int16",
formats: []string{"text", "txt", "plain"},
value: int16(44),
want: "44",
},
{
name: "int32",
formats: []string{"text", "txt", "plain"},
value: int32(45),
want: "45",
},
{
name: "int64",
formats: []string{"text", "txt", "plain"},
value: int64(46),
want: "46",
},
{
name: "uint",
formats: []string{"text", "txt", "plain"},
value: uint(47),
want: "47",
},
{
name: "uint8",
formats: []string{"text", "txt", "plain"},
value: uint8(48),
want: "48",
},
{
name: "uint16",
formats: []string{"text", "txt", "plain"},
value: uint16(49),
want: "49",
},
{
name: "uint32",
formats: []string{"text", "txt", "plain"},
value: uint32(50),
want: "50",
},
{
name: "uint64",
formats: []string{"text", "txt", "plain"},
value: uint64(51),
want: "51",
},
{
name: "float32",
formats: []string{"text", "txt", "plain"},
value: float32(3.14),
want: "3.14",
},
{
name: "float64",
formats: []string{"text", "txt", "plain"},
value: float64(3.14159),
want: "3.14159",
},
{
name: "bool true",
formats: []string{"text", "txt", "plain"},
value: true,
want: "true",
},
{
name: "bool false",
formats: []string{"text", "txt", "plain"},
value: false,
want: "false",
},
{
name: "implements fmt.Stringer",
formats: []string{"text", "txt", "plain"},
value: &mockStringer{value: "test string"},
want: "test string",
},
{
name: "error writing to writer with fmt.Stringer",
formats: []string{"text", "txt", "plain"},
writeErr: errors.New("write error!!1"),
value: &mockStringer{value: "test string"},
wantErr: "render: failed: write error!!1",
wantErrIs: []error{Err, ErrFailed},
},
{
name: "implements io.WriterTo",
formats: []string{"text", "txt", "plain"},
value: &mockWriterTo{value: "test string"},
want: "test string",
},
{
name: "io.WriterTo error",
formats: []string{"text", "txt", "plain"},
value: &mockWriterTo{
value: "test string",
err: errors.New("WriteTo error!!1"),
},
wantErr: "render: failed: WriteTo error!!1",
wantErrIs: []error{Err, ErrFailed},
},
{
name: "implements io.Reader",
formats: []string{"text", "txt", "plain"},
valueFunc: func() any { return &mockReader{value: "reader string"} },
want: "reader string",
},
{
name: "io.Reader error",
formats: []string{"text", "txt", "plain"},
value: &mockReader{
value: "reader string",
err: errors.New("Read error!!1"),
},
wantErr: "render: failed: Read error!!1",
wantErrIs: []error{Err, ErrFailed},
},
{
name: "error",
formats: []string{"text", "txt", "plain"},
value: errors.New("this is an error"),
want: "this is an error",
},
{
name: "does not implement any supported type/interface",
formats: []string{"text", "txt", "plain"},
value: struct{}{},
wantErr: "render: unsupported format: {{format}}",
wantErrIs: []error{Err, ErrUnsupportedFormat},
},
}
// "xml" format.
var xmlFormatTestCases = []renderFormatTestCase{
{
name: "xml format",
formats: []string{"xml"},
value: struct {
XMLName xml.Name `xml:"user"`
Age int `xml:"age"`
}{Age: 30},
wantPretty: "<user>\n <age>30</age>\n</user>",
wantCompact: "<user><age>30</age></user>",
},
{
name: "xml format with xml.Marshaler",
formats: []string{"xml"},
value: &mockXMLMarshaler{elm: "test string"},
want: "<mockXMLMarshaler>test string</mockXMLMarshaler>",
},
{
name: "capitalized format",
formats: []string{"XML"},
value: &mockXMLMarshaler{elm: "test string"},
want: "<mockXMLMarshaler>test string</mockXMLMarshaler>",
},
{
name: "xml format with error from xml.Marshaler",
formats: []string{"xml"},
value: &mockXMLMarshaler{err: errors.New("marshal error!!1")},
wantErr: "render: failed: marshal error!!1",
wantErrIs: []error{Err, ErrFailed},
},
{
name: "xml format with error writing to writer",
formats: []string{"xml"},
writeErr: errors.New("write error!!1"),
value: struct {
XMLName xml.Name `xml:"user"`
Age int `xml:"age"`
}{Age: 30},
wantErr: "render: failed: write error!!1",
wantErrIs: []error{Err, ErrFailed},
},
{
name: "xml format with invalid value",
formats: []string{"xml"},
value: make(chan int),
wantErr: "render: failed: xml: unsupported type: chan int",
wantErrIs: []error{Err, ErrFailed},
},
}
// "yaml" format.
var yamlFormatTestCases = []renderFormatTestCase{
{
name: "yaml format with map",
formats: []string{"yaml", "yml"},
value: map[string]int{"age": 30},
want: "age: 30\n",
},
{
name: "capitalized format",
formats: []string{"YAML", "YML"},
value: map[string]int{"age": 30},
want: "age: 30\n",
},
{
name: "yaml format with nested structure",
formats: []string{"yaml", "yml"},
value: map[string]any{
"user": map[string]any{
"age": 30,
"name": "John Doe",
},
},
want: "user:\n age: 30\n name: John Doe\n",
},
{
name: "yaml format with sequences",
value: map[string]any{
"books": []string{
"The Great Gatsby",
"1984",
},
},
want: "books:\n - The Great Gatsby\n - \"1984\"\n",
},
{
name: "yaml format with yaml.InterfaceMarshaler",
formats: []string{"yaml", "yml"},
value: &mockYAMLInterfaceMarshaler{val: map[string]int{"age": 30}},
want: "age: 30\n",
},
{
name: "yaml format with error from yaml.InterfaceMarshaler",
formats: []string{"yaml", "yml"},
value: &mockYAMLInterfaceMarshaler{err: errors.New("mock error")},
wantErr: "render: failed: mock error",
wantErrIs: []error{Err, ErrFailed},
},
{
name: "yaml format with yaml.BytesMarshaler",
formats: []string{"yaml", "yml"},
value: &mockYAMLBytesMarshaler{val: []byte("age: 30\n")},
want: "age: 30\n",
},
{
name: "yaml format with error from yaml.BytesMarshaler",
formats: []string{"yaml", "yml"},
value: &mockYAMLBytesMarshaler{err: errors.New("mock error")},
wantErr: "render: failed: mock error",
wantErrIs: []error{Err, ErrFailed},
},
{
name: "yaml format with error writing to writer",
formats: []string{"yaml", "yml"},
writeErr: errors.New("write error!!1"),
value: map[string]int{"age": 30},
wantErr: "render: failed: yaml: write error: write error!!1",
wantErrIs: []error{Err, ErrFailed},
},
{
name: "yaml format with invalid type",
formats: []string{"yaml", "yml"},
value: make(chan int),
wantErr: "render: failed: unknown value type chan int",
wantErrIs: []error{Err, ErrFailed},
},
}
func TestRender(t *testing.T) {
tests := []renderFormatTestCase{}
tests = append(tests, jsonFormatTestCases...)
tests = append(tests, textFormatTestCases...)
tests = append(tests, yamlFormatTestCases...)
for _, tt := range tests {
for _, pretty := range []bool{false, true} {
for _, format := range tt.formats {
name := format + " format " + tt.name
if pretty {
name = "pretty " + name
}
t.Run(name, func(t *testing.T) {
w := &mockWriter{WriteErr: tt.writeErr}
value := tt.value
if tt.valueFunc != nil {
value = tt.valueFunc()
}
want := tt.want
if pretty && tt.wantPretty != "" {
want = tt.wantPretty
} else if tt.wantCompact != "" {
want = tt.wantCompact
}
err := Render(w, format, pretty, value)
got := w.String()
if tt.wantErr != "" {
wantErr := strings.ReplaceAll(
tt.wantErr, "{{format}}", format,
)
assert.EqualError(t, err, wantErr)
}
for _, e := range tt.wantErrIs {
assert.ErrorIs(t, err, e)
}
if tt.wantErr == "" && len(tt.wantErrIs) == 0 {
assert.NoError(t, err)
assert.Equal(t, want, got)
}
})
}
}
}
}
func TestPretty(t *testing.T) {
tests := []renderFormatTestCase{}
tests = append(tests, jsonFormatTestCases...)
tests = append(tests, textFormatTestCases...)
tests = append(tests, yamlFormatTestCases...)
for _, tt := range tests {
for _, format := range tt.formats {
t.Run(format+" format "+tt.name, func(t *testing.T) {
w := &mockWriter{WriteErr: tt.writeErr}
value := tt.value
if tt.valueFunc != nil {
value = tt.valueFunc()
}
want := tt.want
if tt.wantPretty != "" {
want = tt.wantPretty
} else if tt.wantCompact != "" {
want = tt.wantCompact
}
err := Pretty(w, format, value)
got := w.String()
if tt.wantErr != "" {
wantErr := strings.ReplaceAll(
tt.wantErr, "{{format}}", format,
)
assert.EqualError(t, err, wantErr)
}
for _, e := range tt.wantErrIs {
assert.ErrorIs(t, err, e)
}
if tt.wantErr == "" && len(tt.wantErrIs) == 0 {
assert.NoError(t, err)
assert.Equal(t, want, got)
}
})
}
}
}
func TestCompact(t *testing.T) {
tests := []renderFormatTestCase{}
tests = append(tests, jsonFormatTestCases...)
tests = append(tests, textFormatTestCases...)
tests = append(tests, yamlFormatTestCases...)
for _, tt := range tests {
for _, format := range tt.formats {
t.Run(format+" format "+tt.name, func(t *testing.T) {
w := &mockWriter{WriteErr: tt.writeErr}
value := tt.value
if tt.valueFunc != nil {
value = tt.valueFunc()
}
want := tt.want
if tt.wantPretty != "" {
want = tt.wantPretty
} else if tt.wantCompact != "" {
want = tt.wantCompact
}
err := Compact(w, format, value)
got := w.String()
if tt.wantErr != "" {
wantErr := strings.ReplaceAll(
tt.wantErr, "{{format}}", format,
)
assert.EqualError(t, err, wantErr)
}
for _, e := range tt.wantErrIs {
assert.ErrorIs(t, err, e)
}
if tt.wantErr == "" && len(tt.wantErrIs) == 0 {
assert.NoError(t, err)
assert.Equal(t, want, got)
}
})
}
}
}
func TestNewWith(t *testing.T) {
tests := []struct {
name string
formats []string
want *Renderer
}{
{
name: "no formats",
formats: nil,
want: &Renderer{
Handlers: map[string]Handler{},
},
},
{
name: "single format",
formats: []string{"json"},
want: &Renderer{
Handlers: map[string]Handler{
"json": &JSON{},
},
},
},
{
name: "multiple formats",
formats: []string{"json", "xml"},
want: &Renderer{
Handlers: map[string]Handler{
"json": &JSON{},
"xml": &XML{},
},
},
},
{
name: "multiple formats with aliases",
formats: []string{"yaml", "text", "binary"},
want: &Renderer{
Handlers: map[string]Handler{
"bin": &Binary{},
"binary": &Binary{},
"plain": &Text{},
"text": &Text{},
"txt": &Text{},
"yaml": &YAML{},
"yml": &YAML{},
},
},
},
{
name: "duplicate formats",
formats: []string{"json", "json", "yaml", "yaml"},
want: &Renderer{
Handlers: map[string]Handler{
"json": &JSON{},
"yaml": &YAML{},
"yml": &YAML{},
},
},
},
{
name: "capitalized formats",
formats: []string{"YAML", "TEXT", "BINARY"},
want: &Renderer{
Handlers: map[string]Handler{
"bin": &Binary{},
"binary": &Binary{},
"plain": &Text{},
"text": &Text{},
"txt": &Text{},
"yaml": &YAML{},
"yml": &YAML{},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NewWith(tt.formats...)
assert.Equal(t, tt.want, got)
})
}
}