refactor: yet another one, someday I might be happy

This commit is contained in:
2024-03-24 03:28:08 +00:00
parent ccc3668fa2
commit de3a9e55a8
18 changed files with 613 additions and 499 deletions

View File

@@ -2,6 +2,8 @@ linters-settings:
funlen: funlen:
lines: 100 lines: 100
statements: 150 statements: 150
goconst:
min-occurrences: 5
gocyclo: gocyclo:
min-complexity: 20 min-complexity: 20
golint: golint:

View File

@@ -14,7 +14,7 @@ var _ FormatRenderer = (*Binary)(nil)
// Render writes result of calling MarshalBinary() on v. If v does not implment // Render writes result of calling MarshalBinary() on v. If v does not implment
// encoding.BinaryMarshaler the ErrCannotRander error will be returned. // encoding.BinaryMarshaler the ErrCannotRander error will be returned.
func (bm *Binary) Render(w io.Writer, v any) error { func (br *Binary) Render(w io.Writer, v any) error {
x, ok := v.(encoding.BinaryMarshaler) x, ok := v.(encoding.BinaryMarshaler)
if !ok { if !ok {
return fmt.Errorf("%w: %T", ErrCannotRender, v) return fmt.Errorf("%w: %T", ErrCannotRender, v)
@@ -32,3 +32,7 @@ func (bm *Binary) Render(w io.Writer, v any) error {
return nil return nil
} }
func (br *Binary) Formats() []string {
return []string{"binary", "bin"}
}

View File

@@ -1,11 +1,10 @@
package render_test package render
import ( import (
"encoding" "encoding"
"errors" "errors"
"testing" "testing"
"github.com/jimeh/go-render"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -38,7 +37,7 @@ func TestBinary_Render(t *testing.T) {
name: "does not implement encoding.BinaryMarshaler", name: "does not implement encoding.BinaryMarshaler",
value: struct{}{}, value: struct{}{},
wantErr: "render: cannot render: struct {}", wantErr: "render: cannot render: struct {}",
wantErrIs: []error{render.Err, render.ErrCannotRender}, wantErrIs: []error{Err, ErrCannotRender},
}, },
{ {
name: "error marshaling", name: "error marshaling",
@@ -47,19 +46,19 @@ func TestBinary_Render(t *testing.T) {
err: errors.New("marshal error!!1"), err: errors.New("marshal error!!1"),
}, },
wantErr: "render: failed: marshal error!!1", wantErr: "render: failed: marshal error!!1",
wantErrIs: []error{render.Err, render.ErrFailed}, wantErrIs: []error{Err, ErrFailed},
}, },
{ {
name: "error writing to writer", name: "error writing to writer",
writeErr: errors.New("write error!!1"), writeErr: errors.New("write error!!1"),
value: &mockBinaryMarshaler{data: []byte("test string")}, value: &mockBinaryMarshaler{data: []byte("test string")},
wantErr: "render: failed: write error!!1", wantErr: "render: failed: write error!!1",
wantErrIs: []error{render.Err, render.ErrFailed}, wantErrIs: []error{Err, ErrFailed},
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
b := &render.Binary{} b := &Binary{}
w := &mockWriter{WriteErr: tt.writeErr} w := &mockWriter{WriteErr: tt.writeErr}
err := b.Render(w, tt.value) err := b.Render(w, tt.value)

12
json.go
View File

@@ -24,11 +24,11 @@ type JSON struct {
var _ FormatRenderer = (*JSON)(nil) var _ FormatRenderer = (*JSON)(nil)
// Render marshals the given value to JSON. // Render marshals the given value to JSON.
func (j *JSON) Render(w io.Writer, v any) error { func (jr *JSON) Render(w io.Writer, v any) error {
enc := json.NewEncoder(w) enc := json.NewEncoder(w)
if j.Pretty { if jr.Pretty {
prefix := j.Prefix prefix := jr.Prefix
indent := j.Indent indent := jr.Indent
if indent == "" { if indent == "" {
indent = " " indent = " "
} }
@@ -43,3 +43,7 @@ func (j *JSON) Render(w io.Writer, v any) error {
return nil return nil
} }
func (jr *JSON) Formats() []string {
return []string{"json"}
}

View File

@@ -1,4 +1,4 @@
package render_test package render
import ( import (
"bytes" "bytes"
@@ -6,7 +6,6 @@ import (
"errors" "errors"
"testing" "testing"
"github.com/jimeh/go-render"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -69,19 +68,19 @@ func TestJSON_Render(t *testing.T) {
{ {
name: "error from json.Marshaler", name: "error from json.Marshaler",
value: &mockJSONMarshaler{err: errors.New("marshal error!!1")}, value: &mockJSONMarshaler{err: errors.New("marshal error!!1")},
wantErrIs: []error{render.Err, render.ErrFailed}, wantErrIs: []error{Err, ErrFailed},
}, },
{ {
name: "invalid value", name: "invalid value",
pretty: false, pretty: false,
value: make(chan int), value: make(chan int),
wantErr: "render: failed: json: unsupported type: chan int", wantErr: "render: failed: json: unsupported type: chan int",
wantErrIs: []error{render.Err, render.ErrFailed}, wantErrIs: []error{Err, ErrFailed},
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
j := &render.JSON{ j := &JSON{
Pretty: tt.pretty, Pretty: tt.pretty,
Prefix: tt.prefix, Prefix: tt.prefix,
Indent: tt.indent, Indent: tt.indent,

View File

@@ -6,17 +6,17 @@ import (
"io" "io"
) )
// MultiRenderer is a renderer that tries multiple renderers until one succeeds. // Multi is a renderer that tries multiple renderers until one succeeds.
type MultiRenderer struct { type Multi struct {
Renderers []FormatRenderer Renderers []FormatRenderer
} }
var _ FormatRenderer = (*MultiRenderer)(nil) var _ FormatRenderer = (*Multi)(nil)
// Render tries each renderer in order until one succeeds. If none succeed, // Render tries each renderer in order until one succeeds. If none succeed,
// ErrCannotRender is returned. If a renderer returns an error that is not // ErrCannotRender is returned. If a renderer returns an error that is not
// ErrCannotRender, that error is returned. // ErrCannotRender, that error is returned.
func (mr *MultiRenderer) Render(w io.Writer, v any) error { func (mr *Multi) Render(w io.Writer, v any) error {
for _, r := range mr.Renderers { for _, r := range mr.Renderers {
err := r.Render(w, v) err := r.Render(w, v)
if err == nil { if err == nil {
@@ -29,3 +29,22 @@ func (mr *MultiRenderer) Render(w io.Writer, v any) error {
return fmt.Errorf("%w: %T", ErrCannotRender, v) return fmt.Errorf("%w: %T", ErrCannotRender, v)
} }
func (mr *Multi) Formats() []string {
formats := make(map[string]struct{})
for _, r := range mr.Renderers {
if x, ok := r.(Formats); ok {
for _, f := range x.Formats() {
formats[f] = struct{}{}
}
}
}
result := make([]string, 0, len(formats))
for f := range formats {
result = append(result, f)
}
return result
}

View File

@@ -1,22 +1,21 @@
package render_test package render
import ( import (
"bytes" "bytes"
"errors" "errors"
"testing" "testing"
"github.com/jimeh/go-render"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestMultiRenderer_Render(t *testing.T) { func TestMultiRenderer_Render(t *testing.T) {
successRenderer := &mockRenderer{output: "success output"} successRenderer := &mockRenderer{output: "success output"}
cannotRenderer := &mockRenderer{err: render.ErrCannotRender} cannotRenderer := &mockRenderer{err: ErrCannotRender}
failRenderer := &mockRenderer{err: errors.New("mock error")} failRenderer := &mockRenderer{err: errors.New("mock error")}
tests := []struct { tests := []struct {
name string name string
renderers []render.FormatRenderer renderers []FormatRenderer
value interface{} value interface{}
want string want string
wantErr string wantErr string
@@ -24,17 +23,17 @@ func TestMultiRenderer_Render(t *testing.T) {
}{ }{
{ {
name: "no renderer can render", name: "no renderer can render",
renderers: []render.FormatRenderer{ renderers: []FormatRenderer{
cannotRenderer, cannotRenderer,
cannotRenderer, cannotRenderer,
}, },
value: "test", value: "test",
wantErr: "render: cannot render: string", wantErr: "render: cannot render: string",
wantErrIs: []error{render.ErrCannotRender}, wantErrIs: []error{ErrCannotRender},
}, },
{ {
name: "one renderer can render", name: "one renderer can render",
renderers: []render.FormatRenderer{ renderers: []FormatRenderer{
cannotRenderer, cannotRenderer,
successRenderer, successRenderer,
cannotRenderer, cannotRenderer,
@@ -44,8 +43,8 @@ func TestMultiRenderer_Render(t *testing.T) {
}, },
{ {
name: "multiple renderers can render", name: "multiple renderers can render",
renderers: []render.FormatRenderer{ renderers: []FormatRenderer{
&mockRenderer{err: render.ErrCannotRender}, &mockRenderer{err: ErrCannotRender},
&mockRenderer{output: "first output"}, &mockRenderer{output: "first output"},
&mockRenderer{output: "second output"}, &mockRenderer{output: "second output"},
}, },
@@ -54,7 +53,7 @@ func TestMultiRenderer_Render(t *testing.T) {
}, },
{ {
name: "first renderer fails", name: "first renderer fails",
renderers: []render.FormatRenderer{ renderers: []FormatRenderer{
failRenderer, failRenderer,
successRenderer, successRenderer,
}, },
@@ -63,7 +62,7 @@ func TestMultiRenderer_Render(t *testing.T) {
}, },
{ {
name: "fails after cannot render", name: "fails after cannot render",
renderers: []render.FormatRenderer{ renderers: []FormatRenderer{
cannotRenderer, cannotRenderer,
failRenderer, failRenderer,
successRenderer, successRenderer,
@@ -73,7 +72,7 @@ func TestMultiRenderer_Render(t *testing.T) {
}, },
{ {
name: "fails after success render", name: "fails after success render",
renderers: []render.FormatRenderer{ renderers: []FormatRenderer{
successRenderer, successRenderer,
failRenderer, failRenderer,
cannotRenderer, cannotRenderer,
@@ -85,7 +84,7 @@ func TestMultiRenderer_Render(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
mr := &render.MultiRenderer{ mr := &Multi{
Renderers: tt.renderers, Renderers: tt.renderers,
} }
var buf bytes.Buffer var buf bytes.Buffer

151
render.go
View File

@@ -39,101 +39,134 @@ type FormatRenderer interface {
Render(w io.Writer, v any) error Render(w io.Writer, v any) error
} }
// Formats is an optional interface that can be implemented by FormatRenderer
// implementations to return a list of formats that the renderer supports. This
// is used by the NewRenderer function to allowing format aliases like "yml" for
// "yaml".
type Formats interface {
Formats() []string
}
var ( var (
// DefaultBinary is the default binary marshaler renderer. It prettyRenderer = New(map[string]FormatRenderer{
// renders values using the encoding.BinaryMarshaler interface. "binary": &Binary{},
DefaultBinary = &Binary{} "json": &JSON{Pretty: true},
"text": &Text{},
"xml": &XML{Pretty: true},
"yaml": &YAML{Indent: 2},
})
compactRenderer = New(map[string]FormatRenderer{
"binary": &Binary{},
"json": &JSON{},
"text": &Text{},
"xml": &XML{},
"yaml": &YAML{},
})
// DefaultJSON is the default JSON renderer. It renders values using the DefaultPretty = prettyRenderer.OnlyWith("json", "text", "xml", "yaml")
// encoding/json package, with pretty printing enabled. DefaultCompact = compactRenderer.OnlyWith("json", "text", "xml", "yaml")
DefaultJSON = &JSON{Pretty: true}
// DefaultText is the default text renderer, used by the package level
// Render function. It renders values using the DefaultStringer and
// DefaultWriterTo renderers. This means a value must implement either the
// fmt.Stringer or io.WriterTo interfaces to be rendered.
DefaultText = &Text{}
// DefaultXML is the default XML renderer. It renders values using the
// encoding/xml package, with pretty printing enabled.
DefaultXML = &XML{Pretty: true}
// DefaultYAML is the default YAML renderer. It renders values using the
// gopkg.in/yaml.v3 package, with an indentation of 2 spaces.
DefaultYAML = &YAML{Indent: 2}
// DefaultRenderer is used by the package level Render function. It supports
// the text", "json", and "yaml" formats. If you need to support another set
// of formats, use the New function to create a custom FormatRenderer.
DefaultRenderer = MustNew("json", "text", "yaml")
) )
// Render renders the given value to the given writer using the given format. // Render renders the given value to the given writer using the given format.
// If pretty is true, the value will be rendered in a pretty way, otherwise it
// will be rendered in a compact way.
//
// By default it supports the following formats:
//
// - "text": Renders values via a myriad of ways.
// - "json": Renders values using the encoding/json package.
// - "yaml": Renders values using the gopkg.in/yaml.v3 package.
// - "xml": Renders values using the encoding/xml package.
//
// If the format is not supported, a ErrUnsupportedFormat error will be
// returned.
func Render(w io.Writer, format string, pretty bool, v any) error {
if pretty {
return DefaultPretty.Render(w, format, v)
}
return DefaultCompact.Render(w, format, v)
}
// Pretty renders the given value to the given writer using the given format.
// The format must be one of the formats supported by the default renderer. // The format must be one of the formats supported by the default renderer.
// //
// By default it supports the following formats: // By default it supports the following formats:
// //
// - "text": Renders values using the fmt.Stringer and io.WriterTo interfaces. // - "text": Renders values via a myriad of ways.
// - "json": Renders values using the encoding/json package, with pretty // - "json": Renders values using the encoding/json package, with pretty
// printing enabled. // printing enabled.
// - "yaml": Renders values using the gopkg.in/yaml.v3 package, with an // - "yaml": Renders values using the gopkg.in/yaml.v3 package, with an
// indentation of 2 spaces. // indentation of 2 spaces.
// - "xml": Renders values using the encoding/xml package, with pretty
// printing enabled.
// //
// If the format is not supported, a ErrUnsupportedFormat error will be // If the format is not supported, a ErrUnsupportedFormat error will be
// returned. // returned.
// //
// If you need to support a custom set of formats, use the New function to // If you need to support a custom set of formats, use the New function to
// create a new FormatRenderer with the formats you need. If you need new custom // create a new Renderer with the formats you need. If you need new custom
// renderers, manually create a new FormatRenderer. // renderers, manually create a new Renderer.
func Render(w io.Writer, format string, v any) error { func Pretty(w io.Writer, format string, v any) error {
return DefaultRenderer.Render(w, format, v) return DefaultPretty.Render(w, format, v)
} }
// New creates a new *FormatRenderer with support for the given formats. // Compact renders the given value to the given writer using the given format.
// The format must be one of the formats supported by the default renderer.
// //
// Supported formats are: // By default it supports the following formats:
// //
// - "binary": Renders values using DefaultBinary. // - "text": Renders values via a myriad of ways..
// - "json": Renders values using DefaultJSON. // - "json": Renders values using the encoding/json package.
// - "text": Renders values using DefaultText. // - "yaml": Renders values using the gopkg.in/yaml.v3 package.
// - "xml": Renders values using DefaultXML. // - "xml": Renders values using the encoding/xml package.
// - "yaml": Renders values using DefaultYAML.
// //
// If an unsupported format is given, an ErrUnsupportedFormat error will be // If the format is not supported, a ErrUnsupportedFormat error will be
// returned. // returned.
func New(formats ...string) (*Renderer, error) { //
renderers := map[string]FormatRenderer{} // If you need to support a custom set of formats, use the New function to
// create a new Renderer with the formats you need. If you need new custom
// renderers, manually create a new Renderer.
func Compact(w io.Writer, format string, v any) error {
return DefaultCompact.Render(w, format, v)
}
// NewCompact returns a new renderer which only supports the specified formats
// and renders structured formats compactly. If no formats are specified, a
// error is returned.
//
// If any of the formats are not supported by, a ErrUnsupported error is
// returned.
func NewCompact(formats ...string) (*Renderer, error) {
if len(formats) == 0 { if len(formats) == 0 {
return nil, fmt.Errorf("%w: no formats specified", Err) return nil, fmt.Errorf("%w: no formats specified", Err)
} }
for _, format := range formats { for _, format := range formats {
switch format { if _, ok := compactRenderer.Renderers[format]; !ok {
case "binary":
renderers[format] = DefaultBinary
case "json":
renderers[format] = DefaultJSON
case "text":
renderers[format] = DefaultText
case "xml":
renderers[format] = DefaultXML
case "yaml":
renderers[format] = DefaultYAML
default:
return nil, fmt.Errorf("%w: %s", ErrUnsupportedFormat, format) return nil, fmt.Errorf("%w: %s", ErrUnsupportedFormat, format)
} }
} }
return NewFormatRenderer(renderers), nil return compactRenderer.OnlyWith(formats...), nil
} }
// MustNew is like New, but panics if an error occurs. // NewPretty returns a new renderer which only supports the specified formats
func MustNew(formats ...string) *Renderer { // and renders structured formats in a pretty way. If no formats are specified,
r, err := New(formats...) // a error is returned.
if err != nil { //
panic(err.Error()) // If any of the formats are not supported by, a ErrUnsupported error is
// returned.
func NewPretty(formats ...string) (*Renderer, error) {
if len(formats) == 0 {
return nil, fmt.Errorf("%w: no formats specified", Err)
} }
return r for _, format := range formats {
if _, ok := prettyRenderer.Renderers[format]; !ok {
return nil, fmt.Errorf("%w: %s", ErrUnsupportedFormat, format)
}
}
return prettyRenderer.OnlyWith(formats...), nil
} }

View File

@@ -37,7 +37,7 @@ func ExampleRender_json() {
// Render the object to JSON. // Render the object to JSON.
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
err := render.Render(buf, "json", data) err := render.Pretty(buf, "json", data)
if err != nil { if err != nil {
fmt.Printf("err: %s\n", err) fmt.Printf("err: %s\n", err)
@@ -96,7 +96,7 @@ func ExampleRender_yaml() {
// Render the object to YAML. // Render the object to YAML.
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
err := render.Render(buf, "yaml", data) err := render.Pretty(buf, "yaml", data)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -144,7 +144,7 @@ func ExampleRender_xml() {
// Create a new renderer that supports XML in addition to default JSON, YAML // Create a new renderer that supports XML in addition to default JSON, YAML
// and Text. // and Text.
r := render.MustNew("json", "text", "xml", "yaml") r, _ := render.NewPretty("json", "text", "xml", "yaml")
// Render the object to XML. // Render the object to XML.
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
@@ -206,7 +206,7 @@ func ExampleRender_textFromByteSlice() {
// Render the object to XML. // Render the object to XML.
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
err := render.Render(buf, "text", data) err := render.Pretty(buf, "text", data)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -221,7 +221,7 @@ func ExampleRender_textFromString() {
// Render the object to XML. // Render the object to XML.
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
err := render.Render(buf, "text", data) err := render.Pretty(buf, "text", data)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -236,7 +236,7 @@ func ExampleRender_textFromIOReader() {
// Render the object to XML. // Render the object to XML.
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
err := render.Render(buf, "text", data) err := render.Pretty(buf, "text", data)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -261,7 +261,7 @@ func ExampleRender_textFromWriterTo() {
// Render the object to XML. // Render the object to XML.
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
err := render.Render(buf, "text", data) err := render.Pretty(buf, "text", data)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -294,7 +294,7 @@ func ExampleRender_textFromStringer() {
// Render the object to XML. // Render the object to XML.
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
err := render.Render(buf, "text", data) err := render.Pretty(buf, "text", data)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@@ -1,13 +1,13 @@
package render_test package render
import ( import (
"bytes" "bytes"
"encoding/xml" "encoding/xml"
"errors" "errors"
"io" "io"
"strings"
"testing" "testing"
"github.com/jimeh/go-render"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -35,7 +35,7 @@ type mockRenderer struct {
err error err error
} }
var _ render.FormatRenderer = (*mockRenderer)(nil) var _ FormatRenderer = (*mockRenderer)(nil)
func (m *mockRenderer) Render(w io.Writer, _ any) error { func (m *mockRenderer) Render(w io.Writer, _ any) error {
_, err := w.Write([]byte(m.output)) _, err := w.Write([]byte(m.output))
@@ -47,231 +47,341 @@ func (m *mockRenderer) Render(w io.Writer, _ any) error {
return err return err
} }
func TestDefaultJSON(t *testing.T) {
assert.Equal(t, &render.JSON{Pretty: true}, render.DefaultJSON)
}
func TestDefaultXML(t *testing.T) {
assert.Equal(t, &render.XML{Pretty: true}, render.DefaultXML)
}
func TestDefaultYAML(t *testing.T) {
assert.Equal(t, &render.YAML{Indent: 2}, render.DefaultYAML)
}
func TestDefaultText(t *testing.T) {
assert.Equal(t, &render.Text{}, render.DefaultText)
}
func TestDefaultBinary(t *testing.T) {
assert.Equal(t, &render.Binary{}, render.DefaultBinary)
}
func TestDefaultRenderer(t *testing.T) {
want := &render.Renderer{
Formats: map[string]render.FormatRenderer{
"json": render.DefaultJSON,
"text": render.DefaultText,
"yaml": render.DefaultYAML,
},
}
assert.Equal(t, want, render.DefaultRenderer)
}
type renderFormatTestCase struct { type renderFormatTestCase struct {
name string name string
writeErr error writeErr error
format string formats []string
value any value any
want string valueFunc func() any
wantErr string want string
wantErrIs []error wantPretty string
wantPanic string wantCompact string
wantErr string
wantErrIs []error
wantPanic string
} }
// "binary" format. // "binary" format.
var binaryFormattestCases = []renderFormatTestCase{ var binaryFormattestCases = []renderFormatTestCase{
{ {
name: "binary format with binary marshaler", name: "with binary marshaler",
format: "binary", formats: []string{"binary", "bin"},
value: &mockBinaryMarshaler{data: []byte("test string")}, value: &mockBinaryMarshaler{data: []byte("test string")},
want: "test string", want: "test string",
}, },
{ {
name: "binary format without binary marshaler", name: "without binary marshaler",
format: "binary", formats: []string{"binary", "bin"},
value: struct{}{}, value: struct{}{},
wantErr: "render: unsupported format: binary", wantErr: "render: unsupported format: {{format}}",
wantErrIs: []error{render.Err, render.ErrUnsupportedFormat}, wantErrIs: []error{Err, ErrUnsupportedFormat},
}, },
{ {
name: "binary format with error marshaling", name: "with error marshaling",
format: "binary", formats: []string{"binary", "bin"},
value: &mockBinaryMarshaler{ value: &mockBinaryMarshaler{
data: []byte("test string"), data: []byte("test string"),
err: errors.New("marshal error!!1"), err: errors.New("marshal error!!1"),
}, },
wantErr: "render: failed: marshal error!!1", wantErr: "render: failed: marshal error!!1",
wantErrIs: []error{render.Err, render.ErrFailed}, wantErrIs: []error{Err, ErrFailed},
}, },
{ {
name: "binary format with error writing to writer", name: "with error writing to writer",
format: "binary", formats: []string{"binary", "bin"},
writeErr: errors.New("write error!!1"), writeErr: errors.New("write error!!1"),
value: &mockBinaryMarshaler{data: []byte("test string")}, value: &mockBinaryMarshaler{data: []byte("test string")},
wantErr: "render: failed: write error!!1", wantErr: "render: failed: write error!!1",
wantErrIs: []error{render.Err, render.ErrFailed}, wantErrIs: []error{Err, ErrFailed},
}, },
{ {
name: "binary format with invalid type", name: "with invalid type",
format: "binary", formats: []string{"binary", "bin"},
value: make(chan int), value: make(chan int),
wantErr: "render: unsupported format: binary", wantErr: "render: unsupported format: {{format}}",
wantErrIs: []error{render.Err, render.ErrUnsupportedFormat}, wantErrIs: []error{Err, ErrUnsupportedFormat},
}, },
} }
// "json" format. // "json" format.
var jsonFormatTestCases = []renderFormatTestCase{ var jsonFormatTestCases = []renderFormatTestCase{
{ {
name: "json format", name: "with map",
format: "json", formats: []string{"json"},
value: map[string]int{"age": 30}, value: map[string]int{"age": 30},
want: "{\n \"age\": 30\n}\n", wantPretty: "{\n \"age\": 30\n}\n",
wantCompact: "{\"age\":30}\n",
}, },
{ {
name: "json format with json marshaler", name: "with json marshaler",
format: "json", formats: []string{"json"},
value: &mockJSONMarshaler{data: []byte(`{"age":30}`)}, value: &mockJSONMarshaler{data: []byte(`{"age":30}`)},
want: "{\n \"age\": 30\n}\n", wantPretty: "{\n \"age\": 30\n}\n",
wantCompact: "{\"age\":30}\n",
}, },
{ {
name: "json format with error from json marshaler", name: "with error from json marshaler",
format: "json", formats: []string{"json"},
value: &mockJSONMarshaler{err: errors.New("marshal error!!1")}, value: &mockJSONMarshaler{err: errors.New("marshal error!!1")},
wantErrIs: []error{render.Err}, wantErrIs: []error{Err},
}, },
{ {
name: "json format with error writing to writer", name: "with error writing to writer",
format: "json", formats: []string{"json"},
writeErr: errors.New("write error!!1"), writeErr: errors.New("write error!!1"),
value: map[string]int{"age": 30}, value: map[string]int{"age": 30},
wantErr: "render: failed: write error!!1", wantErr: "render: failed: write error!!1",
wantErrIs: []error{render.Err, render.ErrFailed}, wantErrIs: []error{Err, ErrFailed},
}, },
{ {
name: "json format with invalid type", name: "with invalid type",
format: "json", formats: []string{"json"},
value: make(chan int), value: make(chan int),
wantErr: "render: failed: json: unsupported type: chan int", wantErr: "render: failed: json: unsupported type: chan int",
wantErrIs: []error{render.Err, render.ErrFailed}, wantErrIs: []error{Err, ErrFailed},
}, },
} }
// "text" format. // "text" format.
var textFormatTestCases = []renderFormatTestCase{ var textFormatTestCases = []renderFormatTestCase{
{ {
name: "text format with fmt.Stringer", name: "nil",
format: "text", formats: []string{"text", "txt", "plain"},
value: &mockStringer{value: "test string"}, value: nil,
want: "test string", wantErr: "render: unsupported format: {{format}}",
wantErrIs: []error{Err, ErrUnsupportedFormat},
}, },
{ {
name: "text format with io.WriterTo", name: "byte slice",
format: "text", formats: []string{"text", "txt", "plain"},
value: &mockWriterTo{value: "test string"}, value: []byte("test byte slice"),
want: "test string", want: "test byte slice",
}, },
{ {
name: "text format without fmt.Stringer or io.WriterTo", name: "nil byte slice",
format: "text", formats: []string{"text", "txt", "plain"},
value: struct{}{}, value: []byte(nil),
wantErr: "render: unsupported format: text", want: "",
wantErrIs: []error{render.Err, render.ErrUnsupportedFormat},
}, },
{ {
name: "text format with error writing to writer", name: "empty byte slice",
format: "text", 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"), writeErr: errors.New("write error!!1"),
value: &mockStringer{value: "test string"}, value: &mockStringer{value: "test string"},
wantErr: "render: failed: write error!!1", wantErr: "render: failed: write error!!1",
wantErrIs: []error{render.Err, render.ErrFailed}, wantErrIs: []error{Err, ErrFailed},
}, },
{ {
name: "text format with error from io.WriterTo", name: "implements io.WriterTo",
format: "text", 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: &mockWriterTo{
value: "test string", value: "test string",
err: errors.New("WriteTo error!!1"), err: errors.New("WriteTo error!!1"),
}, },
wantErr: "render: failed: WriteTo error!!1", wantErr: "render: failed: WriteTo error!!1",
wantErrIs: []error{render.Err, render.ErrFailed}, wantErrIs: []error{Err, ErrFailed},
}, },
{ {
name: "text format with invalid type", name: "implements io.Reader",
format: "text", formats: []string{"text", "txt", "plain"},
value: make(chan int), valueFunc: func() any { return &mockReader{value: "reader string"} },
wantErr: "render: unsupported format: text", want: "reader string",
wantErrIs: []error{render.Err, render.ErrUnsupportedFormat}, },
{
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. // "xml" format.
var xmlFormatTestCases = []renderFormatTestCase{ var xmlFormatTestCases = []renderFormatTestCase{
{ {
name: "xml format", name: "xml format",
format: "xml", formats: []string{"xml"},
value: struct { value: struct {
XMLName xml.Name `xml:"user"` XMLName xml.Name `xml:"user"`
Age int `xml:"age"` Age int `xml:"age"`
}{Age: 30}, }{Age: 30},
want: "<user>\n <age>30</age>\n</user>", wantPretty: "<user>\n <age>30</age>\n</user>",
wantCompact: "<user><age>30</age></user>",
}, },
{ {
name: "xml format with xml.Marshaler", name: "xml format with xml.Marshaler",
format: "xml", formats: []string{"xml"},
value: &mockXMLMarshaler{elm: "test string"}, value: &mockXMLMarshaler{elm: "test string"},
want: "<mockXMLMarshaler>test string</mockXMLMarshaler>", want: "<mockXMLMarshaler>test string</mockXMLMarshaler>",
}, },
{ {
name: "xml format with error from xml.Marshaler", name: "xml format with error from xml.Marshaler",
format: "xml", formats: []string{"xml"},
value: &mockXMLMarshaler{err: errors.New("marshal error!!1")}, value: &mockXMLMarshaler{err: errors.New("marshal error!!1")},
wantErr: "render: failed: marshal error!!1", wantErr: "render: failed: marshal error!!1",
wantErrIs: []error{render.Err, render.ErrFailed}, wantErrIs: []error{Err, ErrFailed},
}, },
{ {
name: "xml format with error writing to writer", name: "xml format with error writing to writer",
format: "xml", formats: []string{"xml"},
writeErr: errors.New("write error!!1"), writeErr: errors.New("write error!!1"),
value: struct { value: struct {
XMLName xml.Name `xml:"user"` XMLName xml.Name `xml:"user"`
Age int `xml:"age"` Age int `xml:"age"`
}{Age: 30}, }{Age: 30},
wantErr: "render: failed: write error!!1", wantErr: "render: failed: write error!!1",
wantErrIs: []error{render.Err, render.ErrFailed}, wantErrIs: []error{Err, ErrFailed},
}, },
{ {
name: "xml format with invalid value", name: "xml format with invalid value",
format: "xml", formats: []string{"xml"},
value: make(chan int), value: make(chan int),
wantErr: "render: failed: xml: unsupported type: chan int", wantErr: "render: failed: xml: unsupported type: chan int",
wantErrIs: []error{render.Err, render.ErrFailed}, wantErrIs: []error{Err, ErrFailed},
}, },
} }
// "yaml" format. // "yaml" format.
var yamlFormatTestCases = []renderFormatTestCase{ var yamlFormatTestCases = []renderFormatTestCase{
{ {
name: "yaml format", name: "yaml format with map",
format: "yaml", formats: []string{"yaml", "yml"},
value: map[string]int{"age": 30}, value: map[string]int{"age": 30},
want: "age: 30\n", want: "age: 30\n",
}, },
{ {
name: "yaml format with nested structure", name: "yaml format with nested structure",
format: "yaml", formats: []string{"yaml", "yml"},
value: map[string]any{ value: map[string]any{
"user": map[string]any{ "user": map[string]any{
"age": 30, "age": 30,
@@ -281,253 +391,148 @@ var yamlFormatTestCases = []renderFormatTestCase{
want: "user:\n age: 30\n name: John Doe\n", want: "user:\n age: 30\n name: John Doe\n",
}, },
{ {
name: "yaml format with yaml.Marshaler", name: "yaml format with yaml.Marshaler",
format: "yaml", formats: []string{"yaml", "yml"},
value: &mockYAMLMarshaler{val: map[string]int{"age": 30}}, value: &mockYAMLMarshaler{val: map[string]int{"age": 30}},
want: "age: 30\n", want: "age: 30\n",
}, },
{ {
name: "yaml format with error from yaml.Marshaler", name: "yaml format with error from yaml.Marshaler",
format: "yaml", formats: []string{"yaml", "yml"},
value: &mockYAMLMarshaler{err: errors.New("mock error")}, value: &mockYAMLMarshaler{err: errors.New("mock error")},
wantErr: "render: failed: mock error", wantErr: "render: failed: mock error",
wantErrIs: []error{render.Err, render.ErrFailed}, wantErrIs: []error{Err, ErrFailed},
}, },
{ {
name: "yaml format with error writing to writer", name: "yaml format with error writing to writer",
format: "yaml", formats: []string{"yaml", "yml"},
writeErr: errors.New("write error!!1"), writeErr: errors.New("write error!!1"),
value: map[string]int{"age": 30}, value: map[string]int{"age": 30},
wantErr: "render: failed: yaml: write error: write error!!1", wantErr: "render: failed: yaml: write error: write error!!1",
wantErrIs: []error{render.Err, render.ErrFailed}, wantErrIs: []error{Err, ErrFailed},
}, },
{ {
name: "yaml format with invalid type", name: "yaml format with invalid type",
format: "yaml", formats: []string{"yaml", "yml"},
value: make(chan int), value: make(chan int),
wantPanic: "cannot marshal type: chan int", wantPanic: "cannot marshal type: chan int",
}, },
} }
func TestRender(t *testing.T) { func TestPretty(t *testing.T) {
tests := []renderFormatTestCase{} tests := []renderFormatTestCase{}
tests = append(tests, jsonFormatTestCases...) tests = append(tests, jsonFormatTestCases...)
tests = append(tests, textFormatTestCases...) tests = append(tests, textFormatTestCases...)
tests = append(tests, yamlFormatTestCases...) tests = append(tests, yamlFormatTestCases...)
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { for _, format := range tt.formats {
w := &mockWriter{WriteErr: tt.writeErr} t.Run(format+" format "+tt.name, func(t *testing.T) {
w := &mockWriter{WriteErr: tt.writeErr}
var err error value := tt.value
var panicRes any if tt.valueFunc != nil {
func() { value = tt.valueFunc()
defer func() { }
if r := recover(); r != nil {
panicRes = r var err error
} var panicRes any
func() {
defer func() {
if r := recover(); r != nil {
panicRes = r
}
}()
err = Pretty(w, format, value)
}() }()
err = render.Render(w, tt.format, tt.value)
}()
got := w.String() got := w.String()
var want string
if tt.wantPretty == "" && tt.wantCompact == "" {
want = tt.want
} else {
want = tt.wantPretty
}
if tt.wantPanic != "" { if tt.wantPanic != "" {
assert.Equal(t, tt.wantPanic, panicRes) assert.Equal(t, tt.wantPanic, panicRes)
} }
if tt.wantErr != "" { if tt.wantErr != "" {
assert.EqualError(t, err, tt.wantErr) wantErr := strings.ReplaceAll(
} tt.wantErr, "{{format}}", format,
for _, e := range tt.wantErrIs { )
assert.ErrorIs(t, err, e) assert.EqualError(t, err, wantErr)
} }
for _, e := range tt.wantErrIs {
assert.ErrorIs(t, err, e)
}
if tt.wantPanic == "" && if tt.wantPanic == "" &&
tt.wantErr == "" && len(tt.wantErrIs) == 0 { tt.wantErr == "" && len(tt.wantErrIs) == 0 {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, tt.want, got) assert.Equal(t, want, got)
} }
}) })
}
} }
} }
func TestNew(t *testing.T) { func TestCompact(t *testing.T) {
tests := []struct { tests := []renderFormatTestCase{}
name string tests = append(tests, jsonFormatTestCases...)
formats []string tests = append(tests, textFormatTestCases...)
want *render.Renderer tests = append(tests, yamlFormatTestCases...)
wantErr string
wantErrIs []error
}{
{
name: "no formats",
formats: []string{},
wantErr: "render: no formats specified",
wantErrIs: []error{render.Err},
},
{
name: "single format",
formats: []string{"json"},
want: &render.Renderer{
Formats: map[string]render.FormatRenderer{
"json": render.DefaultJSON,
},
},
},
{
name: "multiple formats",
formats: []string{"json", "text", "yaml"},
want: &render.Renderer{
Formats: map[string]render.FormatRenderer{
"json": render.DefaultJSON,
"text": render.DefaultText,
"yaml": render.DefaultYAML,
},
},
},
{
name: "invalid format",
formats: []string{"json", "text", "invalid"},
wantErr: "render: unsupported format: invalid",
wantErrIs: []error{render.Err, render.ErrUnsupportedFormat},
},
{
name: "duplicate format",
formats: []string{"json", "text", "json"},
want: &render.Renderer{
Formats: map[string]render.FormatRenderer{
"json": render.DefaultJSON,
"text": render.DefaultText,
},
},
},
{
name: "all formats",
formats: []string{"json", "text", "yaml", "xml", "binary"},
want: &render.Renderer{
Formats: map[string]render.FormatRenderer{
"json": render.DefaultJSON,
"text": render.DefaultText,
"yaml": render.DefaultYAML,
"xml": render.DefaultXML,
"binary": render.DefaultBinary,
},
},
},
}
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { for _, format := range tt.formats {
got, err := render.New(tt.formats...) t.Run(format+" format "+tt.name, func(t *testing.T) {
w := &mockWriter{WriteErr: tt.writeErr}
if tt.wantErr != "" { value := tt.value
assert.EqualError(t, err, tt.wantErr) if tt.valueFunc != nil {
} value = tt.valueFunc()
for _, e := range tt.wantErrIs { }
assert.ErrorIs(t, err, e)
}
if tt.wantErr == "" && len(tt.wantErrIs) == 0 { var err error
assert.NoError(t, err) var panicRes any
assert.Equal(t, tt.want, got) func() {
} defer func() {
}) if r := recover(); r != nil {
} panicRes = r
} }
}()
func TestMustNew(t *testing.T) { err = Compact(w, format, value)
tests := []struct {
name string
formats []string
want *render.Renderer
wantErr string
wantErrIs []error
wantPanic string
}{
{
name: "no formats",
formats: []string{},
wantPanic: "render: no formats specified",
},
{
name: "single format",
formats: []string{"json"},
want: &render.Renderer{
Formats: map[string]render.FormatRenderer{
"json": render.DefaultJSON,
},
},
},
{
name: "multiple formats",
formats: []string{"json", "text", "yaml"},
want: &render.Renderer{
Formats: map[string]render.FormatRenderer{
"json": render.DefaultJSON,
"text": render.DefaultText,
"yaml": render.DefaultYAML,
},
},
},
{
name: "invalid format",
formats: []string{"json", "text", "invalid"},
wantPanic: "render: unsupported format: invalid",
},
{
name: "duplicate format",
formats: []string{"json", "text", "json"},
want: &render.Renderer{
Formats: map[string]render.FormatRenderer{
"json": render.DefaultJSON,
"text": render.DefaultText,
},
},
},
{
name: "all formats",
formats: []string{"json", "text", "yaml", "xml", "binary"},
want: &render.Renderer{
Formats: map[string]render.FormatRenderer{
"json": render.DefaultJSON,
"text": render.DefaultText,
"yaml": render.DefaultYAML,
"xml": render.DefaultXML,
"binary": render.DefaultBinary,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got *render.Renderer
var err error
var panicRes any
func() {
defer func() {
if r := recover(); r != nil {
panicRes = r
}
}() }()
got = render.MustNew(tt.formats...)
}()
if tt.wantPanic != "" { got := w.String()
assert.Equal(t, tt.wantPanic, panicRes) var want string
} if tt.wantPretty == "" && tt.wantCompact == "" {
want = tt.want
} else {
want = tt.wantCompact
}
if tt.wantErr != "" { if tt.wantPanic != "" {
assert.EqualError(t, err, tt.wantErr) assert.Equal(t, tt.wantPanic, panicRes)
} }
for _, e := range tt.wantErrIs {
assert.ErrorIs(t, err, e)
}
if tt.wantPanic == "" && if tt.wantErr != "" {
tt.wantErr == "" && len(tt.wantErrIs) == 0 { wantErr := strings.ReplaceAll(
assert.NoError(t, err) tt.wantErr, "{{format}}", format,
assert.Equal(t, tt.want, got) )
} assert.EqualError(t, err, wantErr)
}) }
for _, e := range tt.wantErrIs {
assert.ErrorIs(t, err, e)
}
if tt.wantPanic == "" &&
tt.wantErr == "" && len(tt.wantErrIs) == 0 {
assert.NoError(t, err)
assert.Equal(t, want, got)
}
})
}
} }
} }

View File

@@ -13,15 +13,32 @@ var ErrUnsupportedFormat = fmt.Errorf("%w: unsupported format", Err)
// Renderer is a renderer that delegates rendering to another renderer // Renderer is a renderer that delegates rendering to another renderer
// based on a format value. // based on a format value.
type Renderer struct { type Renderer struct {
// Formats is a map of format names to renderers. When Render is called, // Renderers is a map of format names to renderers. When Render is called,
// the format is used to look up the renderer to use. // the format is used to look up the renderer to use.
Formats map[string]FormatRenderer Renderers map[string]FormatRenderer
} }
// NewFormatRenderer returns a new FormatRenderer that delegates rendering to // New returns a new Renderer that delegates rendering to the specified
// the specified renderers. // renderers.
func NewFormatRenderer(formats map[string]FormatRenderer) *Renderer { //
return &Renderer{Formats: formats} // Any renderers which implement the Formats interface, will also be set as the
// renderer for all format strings returned by Format() on the renderer.
func New(renderers map[string]FormatRenderer) *Renderer {
newRenderers := make(map[string]FormatRenderer, len(renderers))
for format, r := range renderers {
newRenderers[format] = r
if x, ok := r.(Formats); ok {
for _, f := range x.Formats() {
if f != format {
newRenderers[f] = r
}
}
}
}
return &Renderer{Renderers: newRenderers}
} }
// Render renders a value to an io.Writer using the specified format. If the // Render renders a value to an io.Writer using the specified format. If the
@@ -32,7 +49,7 @@ func NewFormatRenderer(formats map[string]FormatRenderer) *Renderer {
// ErrCannotRender, but it could be a different error if the renderer returns // ErrCannotRender, but it could be a different error if the renderer returns
// one. // one.
func (r *Renderer) Render(w io.Writer, format string, v any) error { func (r *Renderer) Render(w io.Writer, format string, v any) error {
renderer, ok := r.Formats[format] renderer, ok := r.Renderers[format]
if !ok { if !ok {
return fmt.Errorf("%w: %s", ErrUnsupportedFormat, format) return fmt.Errorf("%w: %s", ErrUnsupportedFormat, format)
} }
@@ -51,3 +68,15 @@ func (r *Renderer) Render(w io.Writer, format string, v any) error {
return nil return nil
} }
func (r *Renderer) OnlyWith(formats ...string) *Renderer {
renderers := make(map[string]FormatRenderer, len(formats))
for _, format := range formats {
if r, ok := r.Renderers[format]; ok {
renderers[format] = r
}
}
return New(renderers)
}

View File

@@ -1,12 +1,12 @@
package render_test package render
import ( import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"strings"
"testing" "testing"
"github.com/jimeh/go-render"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -14,7 +14,7 @@ import (
func TestRenderer_Render(t *testing.T) { func TestRenderer_Render(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
renderers map[string]render.FormatRenderer renderers map[string]FormatRenderer
format string format string
value interface{} value interface{}
want string want string
@@ -23,7 +23,7 @@ func TestRenderer_Render(t *testing.T) {
}{ }{
{ {
name: "existing renderer", name: "existing renderer",
renderers: map[string]render.FormatRenderer{ renderers: map[string]FormatRenderer{
"mock": &mockRenderer{output: "mock output"}, "mock": &mockRenderer{output: "mock output"},
}, },
format: "mock", format: "mock",
@@ -32,7 +32,7 @@ func TestRenderer_Render(t *testing.T) {
}, },
{ {
name: "existing renderer returns error", name: "existing renderer returns error",
renderers: map[string]render.FormatRenderer{ renderers: map[string]FormatRenderer{
"other": &mockRenderer{ "other": &mockRenderer{
output: "mock output", output: "mock output",
err: errors.New("mock error"), err: errors.New("mock error"),
@@ -44,30 +44,30 @@ func TestRenderer_Render(t *testing.T) {
}, },
{ {
name: "existing renderer returns ErrCannotRender", name: "existing renderer returns ErrCannotRender",
renderers: map[string]render.FormatRenderer{ renderers: map[string]FormatRenderer{
"other": &mockRenderer{ "other": &mockRenderer{
output: "mock output", output: "mock output",
err: fmt.Errorf("%w: mock", render.ErrCannotRender), err: fmt.Errorf("%w: mock", ErrCannotRender),
}, },
}, },
format: "other", format: "other",
value: struct{}{}, value: struct{}{},
wantErr: "render: unsupported format: other", wantErr: "render: unsupported format: other",
wantErrIs: []error{render.Err, render.ErrUnsupportedFormat}, wantErrIs: []error{Err, ErrUnsupportedFormat},
}, },
{ {
name: "non-existing renderer", name: "non-existing renderer",
renderers: map[string]render.FormatRenderer{}, renderers: map[string]FormatRenderer{},
format: "unknown", format: "unknown",
value: struct{}{}, value: struct{}{},
wantErr: "render: unsupported format: unknown", wantErr: "render: unsupported format: unknown",
wantErrIs: []error{render.Err, render.ErrUnsupportedFormat}, wantErrIs: []error{Err, ErrUnsupportedFormat},
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
fr := &render.Renderer{ fr := &Renderer{
Formats: tt.renderers, Renderers: tt.renderers,
} }
var buf bytes.Buffer var buf bytes.Buffer
@@ -98,41 +98,57 @@ func TestRenderer_RenderAllFormats(t *testing.T) {
tests = append(tests, yamlFormatTestCases...) tests = append(tests, yamlFormatTestCases...)
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { for _, format := range tt.formats {
w := &mockWriter{WriteErr: tt.writeErr} t.Run(format+" format "+tt.name, func(t *testing.T) {
w := &mockWriter{WriteErr: tt.writeErr}
var err error value := tt.value
var panicRes any if tt.valueFunc != nil {
renderer, err := render.New("binary", "json", "text", "xml", "yaml") value = tt.valueFunc()
require.NoError(t, err) }
func() { var err error
defer func() { var panicRes any
if r := recover(); r != nil { renderer := compactRenderer
panicRes = r require.NoError(t, err)
}
func() {
defer func() {
if r := recover(); r != nil {
panicRes = r
}
}()
err = renderer.Render(w, format, value)
}() }()
err = renderer.Render(w, tt.format, tt.value)
}()
got := w.String() got := w.String()
var want string
if tt.wantPretty == "" && tt.wantCompact == "" {
want = tt.want
} else {
want = tt.wantCompact
}
if tt.wantPanic != "" { if tt.wantPanic != "" {
assert.Equal(t, tt.wantPanic, panicRes) assert.Equal(t, tt.wantPanic, panicRes)
} }
if tt.wantErr != "" { if tt.wantErr != "" {
assert.EqualError(t, err, tt.wantErr) wantErr := strings.ReplaceAll(
} tt.wantErr, "{{format}}", format,
for _, e := range tt.wantErrIs { )
assert.ErrorIs(t, err, e) assert.EqualError(t, err, wantErr)
} }
for _, e := range tt.wantErrIs {
assert.ErrorIs(t, err, e)
}
if tt.wantPanic == "" && if tt.wantPanic == "" &&
tt.wantErr == "" && len(tt.wantErrIs) == 0 { tt.wantErr == "" && len(tt.wantErrIs) == 0 {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, tt.want, got) assert.Equal(t, want, got)
} }
}) })
}
} }
} }

View File

@@ -57,3 +57,7 @@ func (t *Text) Render(w io.Writer, v any) error {
return nil return nil
} }
func (t *Text) Formats() []string {
return []string{"text", "txt", "plain"}
}

View File

@@ -1,4 +1,4 @@
package render_test package render
import ( import (
"errors" "errors"
@@ -6,7 +6,6 @@ import (
"io" "io"
"testing" "testing"
"github.com/jimeh/go-render"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -102,7 +101,7 @@ func TestText_Render(t *testing.T) {
name: "nil", name: "nil",
value: nil, value: nil,
wantErr: "render: cannot render: <nil>", wantErr: "render: cannot render: <nil>",
wantErrIs: []error{render.Err, render.ErrCannotRender}, wantErrIs: []error{Err, ErrCannotRender},
}, },
{ {
name: "byte slice", name: "byte slice",
@@ -153,7 +152,7 @@ func TestText_Render(t *testing.T) {
writeErr: errors.New("write error!!1"), writeErr: errors.New("write error!!1"),
value: &mockStringer{value: "test string"}, value: &mockStringer{value: "test string"},
wantErr: "render: failed: write error!!1", wantErr: "render: failed: write error!!1",
wantErrIs: []error{render.Err, render.ErrFailed}, wantErrIs: []error{Err, ErrFailed},
}, },
{ {
name: "implements io.WriterTo", name: "implements io.WriterTo",
@@ -167,7 +166,7 @@ func TestText_Render(t *testing.T) {
err: errors.New("WriteTo error!!1"), err: errors.New("WriteTo error!!1"),
}, },
wantErr: "render: failed: WriteTo error!!1", wantErr: "render: failed: WriteTo error!!1",
wantErrIs: []error{render.Err, render.ErrFailed}, wantErrIs: []error{Err, ErrFailed},
}, },
{ {
name: "implements io.Reader", name: "implements io.Reader",
@@ -181,7 +180,7 @@ func TestText_Render(t *testing.T) {
err: errors.New("Read error!!1"), err: errors.New("Read error!!1"),
}, },
wantErr: "render: failed: Read error!!1", wantErr: "render: failed: Read error!!1",
wantErrIs: []error{render.Err, render.ErrFailed}, wantErrIs: []error{Err, ErrFailed},
}, },
{ {
name: "error", name: "error",
@@ -192,12 +191,12 @@ func TestText_Render(t *testing.T) {
name: "does not implement any supported type/interface", name: "does not implement any supported type/interface",
value: struct{}{}, value: struct{}{},
wantErr: "render: cannot render: struct {}", wantErr: "render: cannot render: struct {}",
wantErrIs: []error{render.Err, render.ErrCannotRender}, wantErrIs: []error{Err, ErrCannotRender},
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
s := &render.Text{} s := &Text{}
w := &mockWriter{WriteErr: tt.writeErr} w := &mockWriter{WriteErr: tt.writeErr}
err := s.Render(w, tt.value) err := s.Render(w, tt.value)
@@ -217,7 +216,3 @@ func TestText_Render(t *testing.T) {
}) })
} }
} }
func ptr[T any](v T) *T {
return &v
}

4
xml.go
View File

@@ -43,3 +43,7 @@ func (x *XML) Render(w io.Writer, v any) error {
return nil return nil
} }
func (x *XML) Formats() []string {
return []string{"xml"}
}

View File

@@ -1,4 +1,4 @@
package render_test package render
import ( import (
"bytes" "bytes"
@@ -6,7 +6,6 @@ import (
"errors" "errors"
"testing" "testing"
"github.com/jimeh/go-render"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -90,20 +89,20 @@ func TestXML_Render(t *testing.T) {
name: "error from xml.Marshaler", name: "error from xml.Marshaler",
value: &mockXMLMarshaler{err: errors.New("mock error")}, value: &mockXMLMarshaler{err: errors.New("mock error")},
wantErr: "render: failed: mock error", wantErr: "render: failed: mock error",
wantErrIs: []error{render.Err, render.ErrFailed}, wantErrIs: []error{Err, ErrFailed},
}, },
{ {
name: "invalid value", name: "invalid value",
pretty: false, pretty: false,
value: make(chan int), value: make(chan int),
wantErr: "render: failed: xml: unsupported type: chan int", wantErr: "render: failed: xml: unsupported type: chan int",
wantErrIs: []error{render.Err, render.ErrFailed}, wantErrIs: []error{Err, ErrFailed},
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
x := &render.XML{ x := &XML{
Pretty: tt.pretty, Pretty: tt.pretty,
Prefix: tt.prefix, Prefix: tt.prefix,
Indent: tt.indent, Indent: tt.indent,

View File

@@ -17,10 +17,10 @@ type YAML struct {
var _ FormatRenderer = (*YAML)(nil) var _ FormatRenderer = (*YAML)(nil)
// Render marshals the given value to YAML. // Render marshals the given value to YAML.
func (j *YAML) Render(w io.Writer, v any) error { func (y *YAML) Render(w io.Writer, v any) error {
enc := yaml.NewEncoder(w) enc := yaml.NewEncoder(w)
indent := j.Indent indent := y.Indent
if indent == 0 { if indent == 0 {
indent = 2 indent = 2
} }
@@ -34,3 +34,7 @@ func (j *YAML) Render(w io.Writer, v any) error {
return nil return nil
} }
func (y *YAML) Formats() []string {
return []string{"yaml", "yml"}
}

View File

@@ -1,11 +1,10 @@
package render_test package render
import ( import (
"bytes" "bytes"
"errors" "errors"
"testing" "testing"
"github.com/jimeh/go-render"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@@ -67,7 +66,7 @@ func TestYAML_Render(t *testing.T) {
name: "error from yaml.Marshaler", name: "error from yaml.Marshaler",
value: &mockYAMLMarshaler{err: errors.New("mock error")}, value: &mockYAMLMarshaler{err: errors.New("mock error")},
wantErr: "render: failed: mock error", wantErr: "render: failed: mock error",
wantErrIs: []error{render.Err, render.ErrFailed}, wantErrIs: []error{Err, ErrFailed},
}, },
{ {
name: "invalid value", name: "invalid value",
@@ -78,7 +77,7 @@ func TestYAML_Render(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
j := &render.YAML{ j := &YAML{
Indent: tt.indent, Indent: tt.indent,
} }