mirror of
https://github.com/jimeh/go-render.git
synced 2026-02-19 03:16:39 +00:00
refactor: yet another one, someday I might be happy
This commit is contained in:
@@ -2,6 +2,8 @@ linters-settings:
|
||||
funlen:
|
||||
lines: 100
|
||||
statements: 150
|
||||
goconst:
|
||||
min-occurrences: 5
|
||||
gocyclo:
|
||||
min-complexity: 20
|
||||
golint:
|
||||
|
||||
@@ -14,7 +14,7 @@ var _ FormatRenderer = (*Binary)(nil)
|
||||
|
||||
// Render writes result of calling MarshalBinary() on v. If v does not implment
|
||||
// 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)
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: %T", ErrCannotRender, v)
|
||||
@@ -32,3 +32,7 @@ func (bm *Binary) Render(w io.Writer, v any) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (br *Binary) Formats() []string {
|
||||
return []string{"binary", "bin"}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package render_test
|
||||
package render
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/jimeh/go-render"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -38,7 +37,7 @@ func TestBinary_Render(t *testing.T) {
|
||||
name: "does not implement encoding.BinaryMarshaler",
|
||||
value: struct{}{},
|
||||
wantErr: "render: cannot render: struct {}",
|
||||
wantErrIs: []error{render.Err, render.ErrCannotRender},
|
||||
wantErrIs: []error{Err, ErrCannotRender},
|
||||
},
|
||||
{
|
||||
name: "error marshaling",
|
||||
@@ -47,19 +46,19 @@ func TestBinary_Render(t *testing.T) {
|
||||
err: errors.New("marshal error!!1"),
|
||||
},
|
||||
wantErr: "render: failed: marshal error!!1",
|
||||
wantErrIs: []error{render.Err, render.ErrFailed},
|
||||
wantErrIs: []error{Err, ErrFailed},
|
||||
},
|
||||
{
|
||||
name: "error writing to writer",
|
||||
writeErr: errors.New("write error!!1"),
|
||||
value: &mockBinaryMarshaler{data: []byte("test string")},
|
||||
wantErr: "render: failed: write error!!1",
|
||||
wantErrIs: []error{render.Err, render.ErrFailed},
|
||||
wantErrIs: []error{Err, ErrFailed},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
b := &render.Binary{}
|
||||
b := &Binary{}
|
||||
w := &mockWriter{WriteErr: tt.writeErr}
|
||||
|
||||
err := b.Render(w, tt.value)
|
||||
|
||||
12
json.go
12
json.go
@@ -24,11 +24,11 @@ type JSON struct {
|
||||
var _ FormatRenderer = (*JSON)(nil)
|
||||
|
||||
// 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)
|
||||
if j.Pretty {
|
||||
prefix := j.Prefix
|
||||
indent := j.Indent
|
||||
if jr.Pretty {
|
||||
prefix := jr.Prefix
|
||||
indent := jr.Indent
|
||||
if indent == "" {
|
||||
indent = " "
|
||||
}
|
||||
@@ -43,3 +43,7 @@ func (j *JSON) Render(w io.Writer, v any) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (jr *JSON) Formats() []string {
|
||||
return []string{"json"}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package render_test
|
||||
package render
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/jimeh/go-render"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -69,19 +68,19 @@ func TestJSON_Render(t *testing.T) {
|
||||
{
|
||||
name: "error from json.Marshaler",
|
||||
value: &mockJSONMarshaler{err: errors.New("marshal error!!1")},
|
||||
wantErrIs: []error{render.Err, render.ErrFailed},
|
||||
wantErrIs: []error{Err, ErrFailed},
|
||||
},
|
||||
{
|
||||
name: "invalid value",
|
||||
pretty: false,
|
||||
value: make(chan int),
|
||||
wantErr: "render: failed: json: unsupported type: chan int",
|
||||
wantErrIs: []error{render.Err, render.ErrFailed},
|
||||
wantErrIs: []error{Err, ErrFailed},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
j := &render.JSON{
|
||||
j := &JSON{
|
||||
Pretty: tt.pretty,
|
||||
Prefix: tt.prefix,
|
||||
Indent: tt.indent,
|
||||
|
||||
27
multi.go
27
multi.go
@@ -6,17 +6,17 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// MultiRenderer is a renderer that tries multiple renderers until one succeeds.
|
||||
type MultiRenderer struct {
|
||||
// Multi is a renderer that tries multiple renderers until one succeeds.
|
||||
type Multi struct {
|
||||
Renderers []FormatRenderer
|
||||
}
|
||||
|
||||
var _ FormatRenderer = (*MultiRenderer)(nil)
|
||||
var _ FormatRenderer = (*Multi)(nil)
|
||||
|
||||
// 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, 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 {
|
||||
err := r.Render(w, v)
|
||||
if err == nil {
|
||||
@@ -29,3 +29,22 @@ func (mr *MultiRenderer) Render(w io.Writer, v any) error {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
package render_test
|
||||
package render
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/jimeh/go-render"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMultiRenderer_Render(t *testing.T) {
|
||||
successRenderer := &mockRenderer{output: "success output"}
|
||||
cannotRenderer := &mockRenderer{err: render.ErrCannotRender}
|
||||
cannotRenderer := &mockRenderer{err: ErrCannotRender}
|
||||
failRenderer := &mockRenderer{err: errors.New("mock error")}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
renderers []render.FormatRenderer
|
||||
renderers []FormatRenderer
|
||||
value interface{}
|
||||
want string
|
||||
wantErr string
|
||||
@@ -24,17 +23,17 @@ func TestMultiRenderer_Render(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "no renderer can render",
|
||||
renderers: []render.FormatRenderer{
|
||||
renderers: []FormatRenderer{
|
||||
cannotRenderer,
|
||||
cannotRenderer,
|
||||
},
|
||||
value: "test",
|
||||
wantErr: "render: cannot render: string",
|
||||
wantErrIs: []error{render.ErrCannotRender},
|
||||
wantErrIs: []error{ErrCannotRender},
|
||||
},
|
||||
{
|
||||
name: "one renderer can render",
|
||||
renderers: []render.FormatRenderer{
|
||||
renderers: []FormatRenderer{
|
||||
cannotRenderer,
|
||||
successRenderer,
|
||||
cannotRenderer,
|
||||
@@ -44,8 +43,8 @@ func TestMultiRenderer_Render(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "multiple renderers can render",
|
||||
renderers: []render.FormatRenderer{
|
||||
&mockRenderer{err: render.ErrCannotRender},
|
||||
renderers: []FormatRenderer{
|
||||
&mockRenderer{err: ErrCannotRender},
|
||||
&mockRenderer{output: "first output"},
|
||||
&mockRenderer{output: "second output"},
|
||||
},
|
||||
@@ -54,7 +53,7 @@ func TestMultiRenderer_Render(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "first renderer fails",
|
||||
renderers: []render.FormatRenderer{
|
||||
renderers: []FormatRenderer{
|
||||
failRenderer,
|
||||
successRenderer,
|
||||
},
|
||||
@@ -63,7 +62,7 @@ func TestMultiRenderer_Render(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "fails after cannot render",
|
||||
renderers: []render.FormatRenderer{
|
||||
renderers: []FormatRenderer{
|
||||
cannotRenderer,
|
||||
failRenderer,
|
||||
successRenderer,
|
||||
@@ -73,7 +72,7 @@ func TestMultiRenderer_Render(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "fails after success render",
|
||||
renderers: []render.FormatRenderer{
|
||||
renderers: []FormatRenderer{
|
||||
successRenderer,
|
||||
failRenderer,
|
||||
cannotRenderer,
|
||||
@@ -85,7 +84,7 @@ func TestMultiRenderer_Render(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mr := &render.MultiRenderer{
|
||||
mr := &Multi{
|
||||
Renderers: tt.renderers,
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
|
||||
151
render.go
151
render.go
@@ -39,101 +39,134 @@ type FormatRenderer interface {
|
||||
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 (
|
||||
// DefaultBinary is the default binary marshaler renderer. It
|
||||
// renders values using the encoding.BinaryMarshaler interface.
|
||||
DefaultBinary = &Binary{}
|
||||
prettyRenderer = New(map[string]FormatRenderer{
|
||||
"binary": &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
|
||||
// encoding/json package, with pretty printing enabled.
|
||||
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")
|
||||
DefaultPretty = prettyRenderer.OnlyWith("json", "text", "xml", "yaml")
|
||||
DefaultCompact = compactRenderer.OnlyWith("json", "text", "xml", "yaml")
|
||||
)
|
||||
|
||||
// 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.
|
||||
//
|
||||
// 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
|
||||
// printing enabled.
|
||||
// - "yaml": Renders values using the gopkg.in/yaml.v3 package, with an
|
||||
// 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
|
||||
// returned.
|
||||
//
|
||||
// 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
|
||||
// renderers, manually create a new FormatRenderer.
|
||||
func Render(w io.Writer, format string, v any) error {
|
||||
return DefaultRenderer.Render(w, format, v)
|
||||
// create a new Renderer with the formats you need. If you need new custom
|
||||
// renderers, manually create a new Renderer.
|
||||
func Pretty(w io.Writer, format string, v any) error {
|
||||
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.
|
||||
// - "json": Renders values using DefaultJSON.
|
||||
// - "text": Renders values using DefaultText.
|
||||
// - "xml": Renders values using DefaultXML.
|
||||
// - "yaml": Renders values using DefaultYAML.
|
||||
// - "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 an unsupported format is given, an ErrUnsupportedFormat error will be
|
||||
// If the format is not supported, a ErrUnsupportedFormat error will be
|
||||
// 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 {
|
||||
return nil, fmt.Errorf("%w: no formats specified", Err)
|
||||
}
|
||||
|
||||
for _, format := range formats {
|
||||
switch format {
|
||||
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:
|
||||
if _, ok := compactRenderer.Renderers[format]; !ok {
|
||||
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.
|
||||
func MustNew(formats ...string) *Renderer {
|
||||
r, err := New(formats...)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
// NewPretty returns a new renderer which only supports the specified formats
|
||||
// and renders structured formats in a pretty way. If no formats are specified,
|
||||
// a error is returned.
|
||||
//
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func ExampleRender_json() {
|
||||
|
||||
// Render the object to JSON.
|
||||
buf := &bytes.Buffer{}
|
||||
err := render.Render(buf, "json", data)
|
||||
err := render.Pretty(buf, "json", data)
|
||||
if err != nil {
|
||||
fmt.Printf("err: %s\n", err)
|
||||
|
||||
@@ -96,7 +96,7 @@ func ExampleRender_yaml() {
|
||||
|
||||
// Render the object to YAML.
|
||||
buf := &bytes.Buffer{}
|
||||
err := render.Render(buf, "yaml", data)
|
||||
err := render.Pretty(buf, "yaml", data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -144,7 +144,7 @@ func ExampleRender_xml() {
|
||||
|
||||
// Create a new renderer that supports XML in addition to default JSON, YAML
|
||||
// and Text.
|
||||
r := render.MustNew("json", "text", "xml", "yaml")
|
||||
r, _ := render.NewPretty("json", "text", "xml", "yaml")
|
||||
|
||||
// Render the object to XML.
|
||||
buf := &bytes.Buffer{}
|
||||
@@ -206,7 +206,7 @@ func ExampleRender_textFromByteSlice() {
|
||||
|
||||
// Render the object to XML.
|
||||
buf := &bytes.Buffer{}
|
||||
err := render.Render(buf, "text", data)
|
||||
err := render.Pretty(buf, "text", data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -221,7 +221,7 @@ func ExampleRender_textFromString() {
|
||||
|
||||
// Render the object to XML.
|
||||
buf := &bytes.Buffer{}
|
||||
err := render.Render(buf, "text", data)
|
||||
err := render.Pretty(buf, "text", data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -236,7 +236,7 @@ func ExampleRender_textFromIOReader() {
|
||||
|
||||
// Render the object to XML.
|
||||
buf := &bytes.Buffer{}
|
||||
err := render.Render(buf, "text", data)
|
||||
err := render.Pretty(buf, "text", data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -261,7 +261,7 @@ func ExampleRender_textFromWriterTo() {
|
||||
|
||||
// Render the object to XML.
|
||||
buf := &bytes.Buffer{}
|
||||
err := render.Render(buf, "text", data)
|
||||
err := render.Pretty(buf, "text", data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -294,7 +294,7 @@ func ExampleRender_textFromStringer() {
|
||||
|
||||
// Render the object to XML.
|
||||
buf := &bytes.Buffer{}
|
||||
err := render.Render(buf, "text", data)
|
||||
err := render.Pretty(buf, "text", data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
661
render_test.go
661
render_test.go
@@ -1,13 +1,13 @@
|
||||
package render_test
|
||||
package render
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jimeh/go-render"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -35,7 +35,7 @@ type mockRenderer struct {
|
||||
err error
|
||||
}
|
||||
|
||||
var _ render.FormatRenderer = (*mockRenderer)(nil)
|
||||
var _ FormatRenderer = (*mockRenderer)(nil)
|
||||
|
||||
func (m *mockRenderer) Render(w io.Writer, _ any) error {
|
||||
_, err := w.Write([]byte(m.output))
|
||||
@@ -47,231 +47,341 @@ func (m *mockRenderer) Render(w io.Writer, _ any) error {
|
||||
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 {
|
||||
name string
|
||||
writeErr error
|
||||
format string
|
||||
value any
|
||||
want string
|
||||
wantErr string
|
||||
wantErrIs []error
|
||||
wantPanic string
|
||||
name string
|
||||
writeErr error
|
||||
formats []string
|
||||
value any
|
||||
valueFunc func() any
|
||||
want string
|
||||
wantPretty string
|
||||
wantCompact string
|
||||
wantErr string
|
||||
wantErrIs []error
|
||||
wantPanic string
|
||||
}
|
||||
|
||||
// "binary" format.
|
||||
var binaryFormattestCases = []renderFormatTestCase{
|
||||
{
|
||||
name: "binary format with binary marshaler",
|
||||
format: "binary",
|
||||
value: &mockBinaryMarshaler{data: []byte("test string")},
|
||||
want: "test string",
|
||||
name: "with binary marshaler",
|
||||
formats: []string{"binary", "bin"},
|
||||
value: &mockBinaryMarshaler{data: []byte("test string")},
|
||||
want: "test string",
|
||||
},
|
||||
{
|
||||
name: "binary format without binary marshaler",
|
||||
format: "binary",
|
||||
name: "without binary marshaler",
|
||||
formats: []string{"binary", "bin"},
|
||||
value: struct{}{},
|
||||
wantErr: "render: unsupported format: binary",
|
||||
wantErrIs: []error{render.Err, render.ErrUnsupportedFormat},
|
||||
wantErr: "render: unsupported format: {{format}}",
|
||||
wantErrIs: []error{Err, ErrUnsupportedFormat},
|
||||
},
|
||||
{
|
||||
name: "binary format with error marshaling",
|
||||
format: "binary",
|
||||
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{render.Err, render.ErrFailed},
|
||||
wantErrIs: []error{Err, ErrFailed},
|
||||
},
|
||||
{
|
||||
name: "binary format with error writing to writer",
|
||||
format: "binary",
|
||||
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{render.Err, render.ErrFailed},
|
||||
wantErrIs: []error{Err, ErrFailed},
|
||||
},
|
||||
{
|
||||
name: "binary format with invalid type",
|
||||
format: "binary",
|
||||
name: "with invalid type",
|
||||
formats: []string{"binary", "bin"},
|
||||
value: make(chan int),
|
||||
wantErr: "render: unsupported format: binary",
|
||||
wantErrIs: []error{render.Err, render.ErrUnsupportedFormat},
|
||||
wantErr: "render: unsupported format: {{format}}",
|
||||
wantErrIs: []error{Err, ErrUnsupportedFormat},
|
||||
},
|
||||
}
|
||||
|
||||
// "json" format.
|
||||
var jsonFormatTestCases = []renderFormatTestCase{
|
||||
{
|
||||
name: "json format",
|
||||
format: "json",
|
||||
value: map[string]int{"age": 30},
|
||||
want: "{\n \"age\": 30\n}\n",
|
||||
name: "with map",
|
||||
formats: []string{"json"},
|
||||
value: map[string]int{"age": 30},
|
||||
wantPretty: "{\n \"age\": 30\n}\n",
|
||||
wantCompact: "{\"age\":30}\n",
|
||||
},
|
||||
{
|
||||
name: "json format with json marshaler",
|
||||
format: "json",
|
||||
value: &mockJSONMarshaler{data: []byte(`{"age":30}`)},
|
||||
want: "{\n \"age\": 30\n}\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: "json format with error from json marshaler",
|
||||
format: "json",
|
||||
name: "with error from json marshaler",
|
||||
formats: []string{"json"},
|
||||
value: &mockJSONMarshaler{err: errors.New("marshal error!!1")},
|
||||
wantErrIs: []error{render.Err},
|
||||
wantErrIs: []error{Err},
|
||||
},
|
||||
{
|
||||
name: "json format with error writing to writer",
|
||||
format: "json",
|
||||
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{render.Err, render.ErrFailed},
|
||||
wantErrIs: []error{Err, ErrFailed},
|
||||
},
|
||||
{
|
||||
name: "json format with invalid type",
|
||||
format: "json",
|
||||
name: "with invalid type",
|
||||
formats: []string{"json"},
|
||||
value: make(chan int),
|
||||
wantErr: "render: failed: json: unsupported type: chan int",
|
||||
wantErrIs: []error{render.Err, render.ErrFailed},
|
||||
wantErrIs: []error{Err, ErrFailed},
|
||||
},
|
||||
}
|
||||
|
||||
// "text" format.
|
||||
var textFormatTestCases = []renderFormatTestCase{
|
||||
{
|
||||
name: "text format with fmt.Stringer",
|
||||
format: "text",
|
||||
value: &mockStringer{value: "test string"},
|
||||
want: "test string",
|
||||
name: "nil",
|
||||
formats: []string{"text", "txt", "plain"},
|
||||
value: nil,
|
||||
wantErr: "render: unsupported format: {{format}}",
|
||||
wantErrIs: []error{Err, ErrUnsupportedFormat},
|
||||
},
|
||||
{
|
||||
name: "text format with io.WriterTo",
|
||||
format: "text",
|
||||
value: &mockWriterTo{value: "test string"},
|
||||
want: "test string",
|
||||
name: "byte slice",
|
||||
formats: []string{"text", "txt", "plain"},
|
||||
value: []byte("test byte slice"),
|
||||
want: "test byte slice",
|
||||
},
|
||||
{
|
||||
name: "text format without fmt.Stringer or io.WriterTo",
|
||||
format: "text",
|
||||
value: struct{}{},
|
||||
wantErr: "render: unsupported format: text",
|
||||
wantErrIs: []error{render.Err, render.ErrUnsupportedFormat},
|
||||
name: "nil byte slice",
|
||||
formats: []string{"text", "txt", "plain"},
|
||||
value: []byte(nil),
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "text format with error writing to writer",
|
||||
format: "text",
|
||||
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{render.Err, render.ErrFailed},
|
||||
wantErrIs: []error{Err, ErrFailed},
|
||||
},
|
||||
{
|
||||
name: "text format with error from io.WriterTo",
|
||||
format: "text",
|
||||
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{render.Err, render.ErrFailed},
|
||||
wantErrIs: []error{Err, ErrFailed},
|
||||
},
|
||||
{
|
||||
name: "text format with invalid type",
|
||||
format: "text",
|
||||
value: make(chan int),
|
||||
wantErr: "render: unsupported format: text",
|
||||
wantErrIs: []error{render.Err, render.ErrUnsupportedFormat},
|
||||
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",
|
||||
format: "xml",
|
||||
name: "xml format",
|
||||
formats: []string{"xml"},
|
||||
value: struct {
|
||||
XMLName xml.Name `xml:"user"`
|
||||
Age int `xml:"age"`
|
||||
}{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",
|
||||
format: "xml",
|
||||
value: &mockXMLMarshaler{elm: "test string"},
|
||||
want: "<mockXMLMarshaler>test string</mockXMLMarshaler>",
|
||||
name: "xml format with xml.Marshaler",
|
||||
formats: []string{"xml"},
|
||||
value: &mockXMLMarshaler{elm: "test string"},
|
||||
want: "<mockXMLMarshaler>test string</mockXMLMarshaler>",
|
||||
},
|
||||
{
|
||||
name: "xml format with error from xml.Marshaler",
|
||||
format: "xml",
|
||||
formats: []string{"xml"},
|
||||
value: &mockXMLMarshaler{err: errors.New("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",
|
||||
format: "xml",
|
||||
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{render.Err, render.ErrFailed},
|
||||
wantErrIs: []error{Err, ErrFailed},
|
||||
},
|
||||
{
|
||||
name: "xml format with invalid value",
|
||||
format: "xml",
|
||||
formats: []string{"xml"},
|
||||
value: make(chan int),
|
||||
wantErr: "render: failed: xml: unsupported type: chan int",
|
||||
wantErrIs: []error{render.Err, render.ErrFailed},
|
||||
wantErrIs: []error{Err, ErrFailed},
|
||||
},
|
||||
}
|
||||
|
||||
// "yaml" format.
|
||||
var yamlFormatTestCases = []renderFormatTestCase{
|
||||
{
|
||||
name: "yaml format",
|
||||
format: "yaml",
|
||||
value: map[string]int{"age": 30},
|
||||
want: "age: 30\n",
|
||||
name: "yaml format with map",
|
||||
formats: []string{"yaml", "yml"},
|
||||
value: map[string]int{"age": 30},
|
||||
want: "age: 30\n",
|
||||
},
|
||||
{
|
||||
name: "yaml format with nested structure",
|
||||
format: "yaml",
|
||||
name: "yaml format with nested structure",
|
||||
formats: []string{"yaml", "yml"},
|
||||
value: map[string]any{
|
||||
"user": map[string]any{
|
||||
"age": 30,
|
||||
@@ -281,253 +391,148 @@ var yamlFormatTestCases = []renderFormatTestCase{
|
||||
want: "user:\n age: 30\n name: John Doe\n",
|
||||
},
|
||||
{
|
||||
name: "yaml format with yaml.Marshaler",
|
||||
format: "yaml",
|
||||
value: &mockYAMLMarshaler{val: map[string]int{"age": 30}},
|
||||
want: "age: 30\n",
|
||||
name: "yaml format with yaml.Marshaler",
|
||||
formats: []string{"yaml", "yml"},
|
||||
value: &mockYAMLMarshaler{val: map[string]int{"age": 30}},
|
||||
want: "age: 30\n",
|
||||
},
|
||||
{
|
||||
name: "yaml format with error from yaml.Marshaler",
|
||||
format: "yaml",
|
||||
formats: []string{"yaml", "yml"},
|
||||
value: &mockYAMLMarshaler{err: errors.New("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",
|
||||
format: "yaml",
|
||||
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{render.Err, render.ErrFailed},
|
||||
wantErrIs: []error{Err, ErrFailed},
|
||||
},
|
||||
{
|
||||
name: "yaml format with invalid type",
|
||||
format: "yaml",
|
||||
formats: []string{"yaml", "yml"},
|
||||
value: make(chan int),
|
||||
wantPanic: "cannot marshal type: chan int",
|
||||
},
|
||||
}
|
||||
|
||||
func TestRender(t *testing.T) {
|
||||
func TestPretty(t *testing.T) {
|
||||
tests := []renderFormatTestCase{}
|
||||
tests = append(tests, jsonFormatTestCases...)
|
||||
tests = append(tests, textFormatTestCases...)
|
||||
tests = append(tests, yamlFormatTestCases...)
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
w := &mockWriter{WriteErr: tt.writeErr}
|
||||
for _, format := range tt.formats {
|
||||
t.Run(format+" format "+tt.name, func(t *testing.T) {
|
||||
w := &mockWriter{WriteErr: tt.writeErr}
|
||||
|
||||
var err error
|
||||
var panicRes any
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
panicRes = r
|
||||
}
|
||||
value := tt.value
|
||||
if tt.valueFunc != nil {
|
||||
value = tt.valueFunc()
|
||||
}
|
||||
|
||||
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 != "" {
|
||||
assert.Equal(t, tt.wantPanic, panicRes)
|
||||
}
|
||||
if tt.wantPanic != "" {
|
||||
assert.Equal(t, tt.wantPanic, panicRes)
|
||||
}
|
||||
|
||||
if tt.wantErr != "" {
|
||||
assert.EqualError(t, err, tt.wantErr)
|
||||
}
|
||||
for _, e := range tt.wantErrIs {
|
||||
assert.ErrorIs(t, err, e)
|
||||
}
|
||||
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.wantPanic == "" &&
|
||||
tt.wantErr == "" && len(tt.wantErrIs) == 0 {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
}
|
||||
})
|
||||
if tt.wantPanic == "" &&
|
||||
tt.wantErr == "" && len(tt.wantErrIs) == 0 {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
formats []string
|
||||
want *render.Renderer
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
func TestCompact(t *testing.T) {
|
||||
tests := []renderFormatTestCase{}
|
||||
tests = append(tests, jsonFormatTestCases...)
|
||||
tests = append(tests, textFormatTestCases...)
|
||||
tests = append(tests, yamlFormatTestCases...)
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := render.New(tt.formats...)
|
||||
for _, format := range tt.formats {
|
||||
t.Run(format+" format "+tt.name, func(t *testing.T) {
|
||||
w := &mockWriter{WriteErr: tt.writeErr}
|
||||
|
||||
if tt.wantErr != "" {
|
||||
assert.EqualError(t, err, tt.wantErr)
|
||||
}
|
||||
for _, e := range tt.wantErrIs {
|
||||
assert.ErrorIs(t, err, e)
|
||||
}
|
||||
value := tt.value
|
||||
if tt.valueFunc != nil {
|
||||
value = tt.valueFunc()
|
||||
}
|
||||
|
||||
if tt.wantErr == "" && len(tt.wantErrIs) == 0 {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustNew(t *testing.T) {
|
||||
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
|
||||
}
|
||||
var err error
|
||||
var panicRes any
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
panicRes = r
|
||||
}
|
||||
}()
|
||||
err = Compact(w, format, value)
|
||||
}()
|
||||
got = render.MustNew(tt.formats...)
|
||||
}()
|
||||
|
||||
if tt.wantPanic != "" {
|
||||
assert.Equal(t, tt.wantPanic, panicRes)
|
||||
}
|
||||
got := w.String()
|
||||
var want string
|
||||
if tt.wantPretty == "" && tt.wantCompact == "" {
|
||||
want = tt.want
|
||||
} else {
|
||||
want = tt.wantCompact
|
||||
}
|
||||
|
||||
if tt.wantErr != "" {
|
||||
assert.EqualError(t, err, tt.wantErr)
|
||||
}
|
||||
for _, e := range tt.wantErrIs {
|
||||
assert.ErrorIs(t, err, e)
|
||||
}
|
||||
if tt.wantPanic != "" {
|
||||
assert.Equal(t, tt.wantPanic, panicRes)
|
||||
}
|
||||
|
||||
if tt.wantPanic == "" &&
|
||||
tt.wantErr == "" && len(tt.wantErrIs) == 0 {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
}
|
||||
})
|
||||
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.wantPanic == "" &&
|
||||
tt.wantErr == "" && len(tt.wantErrIs) == 0 {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
43
renderer.go
43
renderer.go
@@ -13,15 +13,32 @@ var ErrUnsupportedFormat = fmt.Errorf("%w: unsupported format", Err)
|
||||
// Renderer is a renderer that delegates rendering to another renderer
|
||||
// based on a format value.
|
||||
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.
|
||||
Formats map[string]FormatRenderer
|
||||
Renderers map[string]FormatRenderer
|
||||
}
|
||||
|
||||
// NewFormatRenderer returns a new FormatRenderer that delegates rendering to
|
||||
// the specified renderers.
|
||||
func NewFormatRenderer(formats map[string]FormatRenderer) *Renderer {
|
||||
return &Renderer{Formats: formats}
|
||||
// New returns a new Renderer that delegates rendering to the specified
|
||||
// renderers.
|
||||
//
|
||||
// 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
|
||||
@@ -32,7 +49,7 @@ func NewFormatRenderer(formats map[string]FormatRenderer) *Renderer {
|
||||
// ErrCannotRender, but it could be a different error if the renderer returns
|
||||
// one.
|
||||
func (r *Renderer) Render(w io.Writer, format string, v any) error {
|
||||
renderer, ok := r.Formats[format]
|
||||
renderer, ok := r.Renderers[format]
|
||||
if !ok {
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package render_test
|
||||
package render
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jimeh/go-render"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
func TestRenderer_Render(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
renderers map[string]render.FormatRenderer
|
||||
renderers map[string]FormatRenderer
|
||||
format string
|
||||
value interface{}
|
||||
want string
|
||||
@@ -23,7 +23,7 @@ func TestRenderer_Render(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "existing renderer",
|
||||
renderers: map[string]render.FormatRenderer{
|
||||
renderers: map[string]FormatRenderer{
|
||||
"mock": &mockRenderer{output: "mock output"},
|
||||
},
|
||||
format: "mock",
|
||||
@@ -32,7 +32,7 @@ func TestRenderer_Render(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "existing renderer returns error",
|
||||
renderers: map[string]render.FormatRenderer{
|
||||
renderers: map[string]FormatRenderer{
|
||||
"other": &mockRenderer{
|
||||
output: "mock output",
|
||||
err: errors.New("mock error"),
|
||||
@@ -44,30 +44,30 @@ func TestRenderer_Render(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "existing renderer returns ErrCannotRender",
|
||||
renderers: map[string]render.FormatRenderer{
|
||||
renderers: map[string]FormatRenderer{
|
||||
"other": &mockRenderer{
|
||||
output: "mock output",
|
||||
err: fmt.Errorf("%w: mock", render.ErrCannotRender),
|
||||
err: fmt.Errorf("%w: mock", ErrCannotRender),
|
||||
},
|
||||
},
|
||||
format: "other",
|
||||
value: struct{}{},
|
||||
wantErr: "render: unsupported format: other",
|
||||
wantErrIs: []error{render.Err, render.ErrUnsupportedFormat},
|
||||
wantErrIs: []error{Err, ErrUnsupportedFormat},
|
||||
},
|
||||
{
|
||||
name: "non-existing renderer",
|
||||
renderers: map[string]render.FormatRenderer{},
|
||||
renderers: map[string]FormatRenderer{},
|
||||
format: "unknown",
|
||||
value: struct{}{},
|
||||
wantErr: "render: unsupported format: unknown",
|
||||
wantErrIs: []error{render.Err, render.ErrUnsupportedFormat},
|
||||
wantErrIs: []error{Err, ErrUnsupportedFormat},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fr := &render.Renderer{
|
||||
Formats: tt.renderers,
|
||||
fr := &Renderer{
|
||||
Renderers: tt.renderers,
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
|
||||
@@ -98,41 +98,57 @@ func TestRenderer_RenderAllFormats(t *testing.T) {
|
||||
tests = append(tests, yamlFormatTestCases...)
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
w := &mockWriter{WriteErr: tt.writeErr}
|
||||
for _, format := range tt.formats {
|
||||
t.Run(format+" format "+tt.name, func(t *testing.T) {
|
||||
w := &mockWriter{WriteErr: tt.writeErr}
|
||||
|
||||
var err error
|
||||
var panicRes any
|
||||
renderer, err := render.New("binary", "json", "text", "xml", "yaml")
|
||||
require.NoError(t, err)
|
||||
value := tt.value
|
||||
if tt.valueFunc != nil {
|
||||
value = tt.valueFunc()
|
||||
}
|
||||
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
panicRes = r
|
||||
}
|
||||
var err error
|
||||
var panicRes any
|
||||
renderer := compactRenderer
|
||||
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 != "" {
|
||||
assert.Equal(t, tt.wantPanic, panicRes)
|
||||
}
|
||||
if tt.wantPanic != "" {
|
||||
assert.Equal(t, tt.wantPanic, panicRes)
|
||||
}
|
||||
|
||||
if tt.wantErr != "" {
|
||||
assert.EqualError(t, err, tt.wantErr)
|
||||
}
|
||||
for _, e := range tt.wantErrIs {
|
||||
assert.ErrorIs(t, err, e)
|
||||
}
|
||||
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.wantPanic == "" &&
|
||||
tt.wantErr == "" && len(tt.wantErrIs) == 0 {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
}
|
||||
})
|
||||
if tt.wantPanic == "" &&
|
||||
tt.wantErr == "" && len(tt.wantErrIs) == 0 {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4
text.go
4
text.go
@@ -57,3 +57,7 @@ func (t *Text) Render(w io.Writer, v any) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Text) Formats() []string {
|
||||
return []string{"text", "txt", "plain"}
|
||||
}
|
||||
|
||||
19
text_test.go
19
text_test.go
@@ -1,4 +1,4 @@
|
||||
package render_test
|
||||
package render
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/jimeh/go-render"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -102,7 +101,7 @@ func TestText_Render(t *testing.T) {
|
||||
name: "nil",
|
||||
value: nil,
|
||||
wantErr: "render: cannot render: <nil>",
|
||||
wantErrIs: []error{render.Err, render.ErrCannotRender},
|
||||
wantErrIs: []error{Err, ErrCannotRender},
|
||||
},
|
||||
{
|
||||
name: "byte slice",
|
||||
@@ -153,7 +152,7 @@ func TestText_Render(t *testing.T) {
|
||||
writeErr: errors.New("write error!!1"),
|
||||
value: &mockStringer{value: "test string"},
|
||||
wantErr: "render: failed: write error!!1",
|
||||
wantErrIs: []error{render.Err, render.ErrFailed},
|
||||
wantErrIs: []error{Err, ErrFailed},
|
||||
},
|
||||
{
|
||||
name: "implements io.WriterTo",
|
||||
@@ -167,7 +166,7 @@ func TestText_Render(t *testing.T) {
|
||||
err: errors.New("WriteTo error!!1"),
|
||||
},
|
||||
wantErr: "render: failed: WriteTo error!!1",
|
||||
wantErrIs: []error{render.Err, render.ErrFailed},
|
||||
wantErrIs: []error{Err, ErrFailed},
|
||||
},
|
||||
{
|
||||
name: "implements io.Reader",
|
||||
@@ -181,7 +180,7 @@ func TestText_Render(t *testing.T) {
|
||||
err: errors.New("Read error!!1"),
|
||||
},
|
||||
wantErr: "render: failed: Read error!!1",
|
||||
wantErrIs: []error{render.Err, render.ErrFailed},
|
||||
wantErrIs: []error{Err, ErrFailed},
|
||||
},
|
||||
{
|
||||
name: "error",
|
||||
@@ -192,12 +191,12 @@ func TestText_Render(t *testing.T) {
|
||||
name: "does not implement any supported type/interface",
|
||||
value: struct{}{},
|
||||
wantErr: "render: cannot render: struct {}",
|
||||
wantErrIs: []error{render.Err, render.ErrCannotRender},
|
||||
wantErrIs: []error{Err, ErrCannotRender},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &render.Text{}
|
||||
s := &Text{}
|
||||
w := &mockWriter{WriteErr: tt.writeErr}
|
||||
|
||||
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
4
xml.go
@@ -43,3 +43,7 @@ func (x *XML) Render(w io.Writer, v any) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *XML) Formats() []string {
|
||||
return []string{"xml"}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package render_test
|
||||
package render
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/jimeh/go-render"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -90,20 +89,20 @@ func TestXML_Render(t *testing.T) {
|
||||
name: "error from xml.Marshaler",
|
||||
value: &mockXMLMarshaler{err: errors.New("mock error")},
|
||||
wantErr: "render: failed: mock error",
|
||||
wantErrIs: []error{render.Err, render.ErrFailed},
|
||||
wantErrIs: []error{Err, ErrFailed},
|
||||
},
|
||||
{
|
||||
name: "invalid value",
|
||||
pretty: false,
|
||||
value: make(chan int),
|
||||
wantErr: "render: failed: xml: unsupported type: chan int",
|
||||
wantErrIs: []error{render.Err, render.ErrFailed},
|
||||
wantErrIs: []error{Err, ErrFailed},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
x := &render.XML{
|
||||
x := &XML{
|
||||
Pretty: tt.pretty,
|
||||
Prefix: tt.prefix,
|
||||
Indent: tt.indent,
|
||||
|
||||
8
yaml.go
8
yaml.go
@@ -17,10 +17,10 @@ type YAML struct {
|
||||
var _ FormatRenderer = (*YAML)(nil)
|
||||
|
||||
// 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)
|
||||
|
||||
indent := j.Indent
|
||||
indent := y.Indent
|
||||
if indent == 0 {
|
||||
indent = 2
|
||||
}
|
||||
@@ -34,3 +34,7 @@ func (j *YAML) Render(w io.Writer, v any) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (y *YAML) Formats() []string {
|
||||
return []string{"yaml", "yml"}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package render_test
|
||||
package render
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/jimeh/go-render"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
@@ -67,7 +66,7 @@ func TestYAML_Render(t *testing.T) {
|
||||
name: "error from yaml.Marshaler",
|
||||
value: &mockYAMLMarshaler{err: errors.New("mock error")},
|
||||
wantErr: "render: failed: mock error",
|
||||
wantErrIs: []error{render.Err, render.ErrFailed},
|
||||
wantErrIs: []error{Err, ErrFailed},
|
||||
},
|
||||
{
|
||||
name: "invalid value",
|
||||
@@ -78,7 +77,7 @@ func TestYAML_Render(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
j := &render.YAML{
|
||||
j := &YAML{
|
||||
Indent: tt.indent,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user