mirror of
https://github.com/jimeh/go-render.git
synced 2026-02-19 03:16:39 +00:00
refactor: focus around Render/Compact/Pretty/NewWith functions
This is yet another drastic refactor of public API and concepts. Hopefully the last one, as I'm now fairly happy with things.
This commit is contained in:
@@ -10,7 +10,10 @@ import (
|
||||
// interface.
|
||||
type Binary struct{}
|
||||
|
||||
var _ FormatRenderer = (*Binary)(nil)
|
||||
var (
|
||||
_ Handler = (*Binary)(nil)
|
||||
_ FormatsHandler = (*Binary)(nil)
|
||||
)
|
||||
|
||||
// Render writes result of calling MarshalBinary() on v. If v does not implment
|
||||
// encoding.BinaryMarshaler the ErrCannotRander error will be returned.
|
||||
@@ -33,6 +36,7 @@ func (br *Binary) Render(w io.Writer, v any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Formats returns a list of format strings that this Handler supports.
|
||||
func (br *Binary) Formats() []string {
|
||||
return []string{"binary", "bin"}
|
||||
}
|
||||
|
||||
@@ -78,3 +78,9 @@ func TestBinary_Render(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBinary_Formats(t *testing.T) {
|
||||
h := &Binary{}
|
||||
|
||||
assert.Equal(t, []string{"binary", "bin"}, h.Formats())
|
||||
}
|
||||
|
||||
41
interfaces.go
Normal file
41
interfaces.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package render
|
||||
|
||||
import "io"
|
||||
|
||||
// Handler interface is for single format renderers, which can only render a
|
||||
// single format. It is the basis of the multi-format support offerred by the
|
||||
// render package.
|
||||
type Handler interface {
|
||||
// Render writes v into w in the format that the Handler supports.
|
||||
//
|
||||
// If v does not implement a required interface, or otherwise cannot be
|
||||
// rendered to the format in question, then a ErrCannotRender error must be
|
||||
// returned. Any other errors should be returned as is.
|
||||
Render(w io.Writer, v any) error
|
||||
}
|
||||
|
||||
// PrettyHandler interface is a optional interface that can be implemented by
|
||||
// Handler implementations to render a value in a pretty way. This is
|
||||
// useful for formats that support pretty printing, like in the case of JSON and
|
||||
// XML.
|
||||
type PrettyHandler interface {
|
||||
// RenderPretty writes v into w in the format that the Handler supports,
|
||||
// using a pretty variant of the format. The exact definition of "pretty" is
|
||||
// up to the handler. Typically this would be mean adding line breaks and
|
||||
// indentation, like in the case of JSON and XML.
|
||||
//
|
||||
// If v does not implement a required interface, or otherwise cannot be
|
||||
// rendered to the format in question, then a ErrCannotRender error must be
|
||||
// returned. Any other errors should be returned as is.
|
||||
RenderPretty(w io.Writer, v any) error
|
||||
}
|
||||
|
||||
// FormatsHandler is an optional interface that can be implemented by Handler
|
||||
// implementations to return a list of formats that the handler supports. This
|
||||
// is used by the New function to allow format aliases like "yml" for "yaml".
|
||||
type FormatsHandler interface {
|
||||
// Formats returns a list of strings which all target the same format. In
|
||||
// most cases this would just be a single value, but multiple values are
|
||||
// supported for the sake of aliases, like "yaml" and "yml".
|
||||
Formats() []string
|
||||
}
|
||||
42
json.go
42
json.go
@@ -6,35 +6,48 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// JSON is a renderer that marshals values to JSON.
|
||||
type JSON struct {
|
||||
// Pretty specifies whether the output should be pretty-printed. If true,
|
||||
// the output will be indented and newlines will be added.
|
||||
Pretty bool
|
||||
// JSONDefualtIndent is the default indentation string used by JSON instances
|
||||
// when pretty rendering if no Indent value is set on the JSON instance itself.
|
||||
var JSONDefualtIndent = " "
|
||||
|
||||
// Prefix is the prefix added to each level of indentation when Pretty is
|
||||
// true.
|
||||
// JSON is a Handler that marshals values to JSON.
|
||||
type JSON struct {
|
||||
// Prefix is the prefix added to each level of indentation when pretty
|
||||
// rendering.
|
||||
Prefix string
|
||||
|
||||
// Indent is the string added to each level of indentation when Pretty is
|
||||
// true. If empty, two spaces will be used instead.
|
||||
// Indent is the string added to each level of indentation when pretty
|
||||
// rendering. If empty, two spaces will be used instead.
|
||||
Indent string
|
||||
}
|
||||
|
||||
var _ FormatRenderer = (*JSON)(nil)
|
||||
var (
|
||||
_ Handler = (*JSON)(nil)
|
||||
_ PrettyHandler = (*JSON)(nil)
|
||||
_ FormatsHandler = (*JSON)(nil)
|
||||
)
|
||||
|
||||
// Render marshals the given value to JSON.
|
||||
func (jr *JSON) Render(w io.Writer, v any) error {
|
||||
enc := json.NewEncoder(w)
|
||||
if jr.Pretty {
|
||||
err := json.NewEncoder(w).Encode(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrFailed, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RenderPretty marshals the given value to JSON with line breaks and
|
||||
// indentation.
|
||||
func (jr *JSON) RenderPretty(w io.Writer, v any) error {
|
||||
prefix := jr.Prefix
|
||||
indent := jr.Indent
|
||||
if indent == "" {
|
||||
indent = " "
|
||||
indent = JSONDefualtIndent
|
||||
}
|
||||
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetIndent(prefix, indent)
|
||||
}
|
||||
|
||||
err := enc.Encode(v)
|
||||
if err != nil {
|
||||
@@ -44,6 +57,7 @@ func (jr *JSON) Render(w io.Writer, v any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Formats returns a list of format strings that this Handler supports.
|
||||
func (jr *JSON) Formats() []string {
|
||||
return []string{"json"}
|
||||
}
|
||||
|
||||
99
json_test.go
99
json_test.go
@@ -24,37 +24,21 @@ func (mjm *mockJSONMarshaler) MarshalJSON() ([]byte, error) {
|
||||
func TestJSON_Render(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pretty bool
|
||||
prefix string
|
||||
indent string
|
||||
value interface{}
|
||||
value any
|
||||
want string
|
||||
wantPretty string
|
||||
wantErr string
|
||||
wantErrIs []error
|
||||
}{
|
||||
{
|
||||
name: "simple object without pretty",
|
||||
pretty: false,
|
||||
name: "simple object",
|
||||
value: map[string]int{"age": 30},
|
||||
want: "{\"age\":30}\n",
|
||||
},
|
||||
{
|
||||
name: "simple object with pretty",
|
||||
pretty: true,
|
||||
value: map[string]int{"age": 30},
|
||||
want: "{\n \"age\": 30\n}\n",
|
||||
},
|
||||
{
|
||||
name: "pretty with prefix and indent",
|
||||
pretty: true,
|
||||
prefix: "// ",
|
||||
indent: "\t",
|
||||
value: map[string]int{"age": 30},
|
||||
want: "{\n// \t\"age\": 30\n// }\n",
|
||||
},
|
||||
{
|
||||
name: "prefix and indent without pretty",
|
||||
pretty: false,
|
||||
name: "ignores prefix and indent",
|
||||
prefix: "// ",
|
||||
indent: "\t",
|
||||
value: map[string]int{"age": 30},
|
||||
@@ -72,7 +56,6 @@ func TestJSON_Render(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "invalid value",
|
||||
pretty: false,
|
||||
value: make(chan int),
|
||||
wantErr: "render: failed: json: unsupported type: chan int",
|
||||
wantErrIs: []error{Err, ErrFailed},
|
||||
@@ -81,7 +64,6 @@ func TestJSON_Render(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
j := &JSON{
|
||||
Pretty: tt.pretty,
|
||||
Prefix: tt.prefix,
|
||||
Indent: tt.indent,
|
||||
}
|
||||
@@ -105,3 +87,76 @@ func TestJSON_Render(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSON_RenderPretty(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
prefix string
|
||||
indent string
|
||||
value any
|
||||
want string
|
||||
wantPretty string
|
||||
wantErr string
|
||||
wantErrIs []error
|
||||
}{
|
||||
{
|
||||
name: "simple object",
|
||||
value: map[string]int{"age": 30},
|
||||
want: "{\n \"age\": 30\n}\n",
|
||||
},
|
||||
{
|
||||
name: "uses prefix and indent",
|
||||
prefix: "// ",
|
||||
indent: "\t",
|
||||
value: map[string]int{"age": 30},
|
||||
want: "{\n// \t\"age\": 30\n// }\n",
|
||||
},
|
||||
{
|
||||
name: "implements json.Marshaler",
|
||||
value: &mockJSONMarshaler{data: []byte(`{"age":30}`)},
|
||||
want: "{\n \"age\": 30\n}\n",
|
||||
},
|
||||
{
|
||||
name: "error from json.Marshaler",
|
||||
value: &mockJSONMarshaler{err: errors.New("marshal error!!1")},
|
||||
wantErrIs: []error{Err, ErrFailed},
|
||||
},
|
||||
{
|
||||
name: "invalid value",
|
||||
value: make(chan int),
|
||||
wantErr: "render: failed: json: unsupported type: chan int",
|
||||
wantErrIs: []error{Err, ErrFailed},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
j := &JSON{
|
||||
Prefix: tt.prefix,
|
||||
Indent: tt.indent,
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
|
||||
err := j.RenderPretty(&buf, tt.value)
|
||||
got := buf.String()
|
||||
|
||||
if tt.wantErr != "" {
|
||||
require.Error(t, err)
|
||||
assert.EqualError(t, err, tt.wantErr)
|
||||
}
|
||||
for _, e := range tt.wantErrIs {
|
||||
assert.ErrorIs(t, err, e)
|
||||
}
|
||||
|
||||
if tt.wantErr == "" && len(tt.wantErrIs) == 0 {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSON_Formats(t *testing.T) {
|
||||
h := &JSON{}
|
||||
|
||||
assert.Equal(t, []string{"json"}, h.Formats())
|
||||
}
|
||||
|
||||
46
multi.go
46
multi.go
@@ -6,18 +6,22 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// Multi is a renderer that tries multiple renderers until one succeeds.
|
||||
// Multi is a Handler that tries multiple handlers until one succeeds.
|
||||
type Multi struct {
|
||||
Renderers []FormatRenderer
|
||||
Handlers []Handler
|
||||
}
|
||||
|
||||
var _ FormatRenderer = (*Multi)(nil)
|
||||
var (
|
||||
_ Handler = (*Multi)(nil)
|
||||
_ PrettyHandler = (*Multi)(nil)
|
||||
_ FormatsHandler = (*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
|
||||
// Render tries each handler in order until one succeeds. If none succeed,
|
||||
// ErrCannotRender is returned. If a handler returns an error that is not
|
||||
// ErrCannotRender, that error is returned.
|
||||
func (mr *Multi) Render(w io.Writer, v any) error {
|
||||
for _, r := range mr.Renderers {
|
||||
for _, r := range mr.Handlers {
|
||||
err := r.Render(w, v)
|
||||
if err == nil {
|
||||
return nil
|
||||
@@ -30,11 +34,37 @@ func (mr *Multi) Render(w io.Writer, v any) error {
|
||||
return fmt.Errorf("%w: %T", ErrCannotRender, v)
|
||||
}
|
||||
|
||||
// RenderPretty tries each handler in order until one succeeds. If none
|
||||
// succeed, ErrCannotRender is returned. If a handler returns an error that is
|
||||
// not ErrCannotRender, that error is returned.
|
||||
//
|
||||
// If a handler implements PrettyHandler, then the RenderPretty method is used
|
||||
// instead of Render. Otherwise, the Render method is used.
|
||||
func (mr *Multi) RenderPretty(w io.Writer, v any) error {
|
||||
for _, r := range mr.Handlers {
|
||||
var err error
|
||||
if x, ok := r.(PrettyHandler); ok {
|
||||
err = x.RenderPretty(w, v)
|
||||
} else {
|
||||
err = r.Render(w, v)
|
||||
}
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, ErrCannotRender) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("%w: %T", ErrCannotRender, v)
|
||||
}
|
||||
|
||||
// Formats returns a list of format strings that this Handler supports.
|
||||
func (mr *Multi) Formats() []string {
|
||||
formats := make(map[string]struct{})
|
||||
|
||||
for _, r := range mr.Renderers {
|
||||
if x, ok := r.(Formats); ok {
|
||||
for _, r := range mr.Handlers {
|
||||
if x, ok := r.(FormatsHandler); ok {
|
||||
for _, f := range x.Formats() {
|
||||
formats[f] = struct{}{}
|
||||
}
|
||||
|
||||
202
multi_test.go
202
multi_test.go
@@ -8,84 +8,129 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMultiRenderer_Render(t *testing.T) {
|
||||
successRenderer := &mockRenderer{output: "success output"}
|
||||
cannotRenderer := &mockRenderer{err: ErrCannotRender}
|
||||
failRenderer := &mockRenderer{err: errors.New("mock error")}
|
||||
|
||||
tests := []struct {
|
||||
var multiHandlerTestCases = []struct {
|
||||
name string
|
||||
renderers []FormatRenderer
|
||||
value interface{}
|
||||
handlers []Handler
|
||||
value any
|
||||
want string
|
||||
wantPretty string
|
||||
wantErr string
|
||||
wantErrIs []error
|
||||
}{
|
||||
{
|
||||
name: "no renderer can render",
|
||||
renderers: []FormatRenderer{
|
||||
cannotRenderer,
|
||||
cannotRenderer,
|
||||
name: "no handler can render",
|
||||
handlers: []Handler{
|
||||
&mockHandler{err: ErrCannotRender},
|
||||
&mockHandler{err: ErrCannotRender},
|
||||
},
|
||||
value: "test",
|
||||
wantErr: "render: cannot render: string",
|
||||
wantErrIs: []error{ErrCannotRender},
|
||||
},
|
||||
{
|
||||
name: "one renderer can render",
|
||||
renderers: []FormatRenderer{
|
||||
cannotRenderer,
|
||||
successRenderer,
|
||||
cannotRenderer,
|
||||
name: "one handler can render",
|
||||
handlers: []Handler{
|
||||
&mockHandler{err: ErrCannotRender},
|
||||
&mockHandler{output: "success output"},
|
||||
&mockHandler{err: ErrCannotRender},
|
||||
},
|
||||
value: struct{}{},
|
||||
want: "success output",
|
||||
wantPretty: "success output",
|
||||
},
|
||||
{
|
||||
name: "multiple renderers can render",
|
||||
renderers: []FormatRenderer{
|
||||
&mockRenderer{err: ErrCannotRender},
|
||||
&mockRenderer{output: "first output"},
|
||||
&mockRenderer{output: "second output"},
|
||||
name: "one pretty handler can render",
|
||||
handlers: []Handler{
|
||||
&mockHandler{err: ErrCannotRender},
|
||||
&mockPrettyHandler{
|
||||
output: "success output",
|
||||
prettyOutput: "pretty success output",
|
||||
},
|
||||
&mockHandler{err: ErrCannotRender},
|
||||
},
|
||||
value: struct{}{},
|
||||
want: "success output",
|
||||
wantPretty: "pretty success output",
|
||||
},
|
||||
{
|
||||
name: "multiple handlers can render",
|
||||
handlers: []Handler{
|
||||
&mockHandler{err: ErrCannotRender},
|
||||
&mockHandler{output: "first output"},
|
||||
&mockHandler{output: "second output"},
|
||||
},
|
||||
value: struct{}{},
|
||||
want: "first output",
|
||||
wantPretty: "first output",
|
||||
},
|
||||
{
|
||||
name: "first renderer fails",
|
||||
renderers: []FormatRenderer{
|
||||
failRenderer,
|
||||
successRenderer,
|
||||
name: "multiple pretty handlers can render",
|
||||
handlers: []Handler{
|
||||
&mockHandler{err: ErrCannotRender},
|
||||
&mockPrettyHandler{
|
||||
output: "first output",
|
||||
prettyOutput: "pretty first output",
|
||||
},
|
||||
&mockPrettyHandler{
|
||||
output: "second output",
|
||||
prettyOutput: "pretty second output",
|
||||
},
|
||||
},
|
||||
value: struct{}{},
|
||||
want: "first output",
|
||||
wantPretty: "pretty first output",
|
||||
},
|
||||
{
|
||||
name: "first handler fails",
|
||||
handlers: []Handler{
|
||||
&mockHandler{err: errors.New("mock error")},
|
||||
&mockHandler{output: "success output"},
|
||||
},
|
||||
value: struct{}{},
|
||||
wantErr: "mock error",
|
||||
},
|
||||
{
|
||||
name: "fails after cannot render",
|
||||
renderers: []FormatRenderer{
|
||||
cannotRenderer,
|
||||
failRenderer,
|
||||
successRenderer,
|
||||
handlers: []Handler{
|
||||
&mockHandler{err: ErrCannotRender},
|
||||
&mockHandler{err: errors.New("mock error")},
|
||||
&mockHandler{output: "success output"},
|
||||
},
|
||||
value: struct{}{},
|
||||
wantErr: "mock error",
|
||||
},
|
||||
{
|
||||
name: "fails after success render",
|
||||
renderers: []FormatRenderer{
|
||||
successRenderer,
|
||||
failRenderer,
|
||||
cannotRenderer,
|
||||
handlers: []Handler{
|
||||
&mockHandler{output: "success output"},
|
||||
&mockHandler{err: errors.New("mock error")},
|
||||
&mockHandler{err: ErrCannotRender},
|
||||
},
|
||||
value: struct{}{},
|
||||
want: "success output",
|
||||
wantPretty: "success output",
|
||||
},
|
||||
{
|
||||
name: "fails after success render with prettier handlers",
|
||||
handlers: []Handler{
|
||||
&mockPrettyHandler{
|
||||
output: "success output",
|
||||
prettyOutput: "pretty success output",
|
||||
},
|
||||
&mockHandler{err: errors.New("mock error")},
|
||||
&mockHandler{err: ErrCannotRender},
|
||||
},
|
||||
value: struct{}{},
|
||||
want: "success output",
|
||||
wantPretty: "pretty success output",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
func TestMulti_Render(t *testing.T) {
|
||||
for _, tt := range multiHandlerTestCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mr := &Multi{
|
||||
Renderers: tt.renderers,
|
||||
Handlers: tt.handlers,
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
|
||||
@@ -106,3 +151,88 @@ func TestMultiRenderer_Render(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMulti_RenderPretty(t *testing.T) {
|
||||
for _, tt := range multiHandlerTestCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mr := &Multi{
|
||||
Handlers: tt.handlers,
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
|
||||
err := mr.RenderPretty(&buf, tt.value)
|
||||
got := buf.String()
|
||||
|
||||
if tt.wantErr != "" {
|
||||
assert.EqualError(t, err, tt.wantErr)
|
||||
}
|
||||
for _, e := range tt.wantErrIs {
|
||||
assert.ErrorIs(t, err, e)
|
||||
}
|
||||
|
||||
if tt.wantErr == "" && len(tt.wantErrIs) == 0 {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantPretty, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMulti_Formats(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
handlers []Handler
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "single handler without a Formats method",
|
||||
handlers: []Handler{
|
||||
&mockHandler{},
|
||||
},
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
name: "multiple handlers without a Formats method",
|
||||
handlers: []Handler{
|
||||
&mockHandler{},
|
||||
},
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
name: "single handler with a Formats method",
|
||||
handlers: []Handler{
|
||||
&mockFormatsHandler{formats: []string{"yaml", "yml"}},
|
||||
},
|
||||
want: []string{"yaml", "yml"},
|
||||
},
|
||||
{
|
||||
name: "multiple handlers without a Formats method",
|
||||
handlers: []Handler{
|
||||
&mockFormatsHandler{formats: []string{"yaml", "yml"}},
|
||||
&mockFormatsHandler{formats: []string{"text", "txt"}},
|
||||
},
|
||||
want: []string{"yaml", "yml", "text", "txt"},
|
||||
},
|
||||
{
|
||||
name: "mixture of handlers with and without a Formats method",
|
||||
handlers: []Handler{
|
||||
&mockFormatsHandler{formats: []string{"yaml", "yml"}},
|
||||
&mockHandler{},
|
||||
&mockFormatsHandler{formats: []string{"binary", "bin"}},
|
||||
&mockHandler{},
|
||||
},
|
||||
want: []string{"yaml", "yml", "binary", "bin"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mr := &Multi{
|
||||
Handlers: tt.handlers,
|
||||
}
|
||||
|
||||
got := mr.Formats()
|
||||
|
||||
assert.ElementsMatch(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
164
render.go
164
render.go
@@ -1,14 +1,20 @@
|
||||
// Package render provides a simple and flexible way to render a value to a
|
||||
// io.Writer using different formats based on a format string argument.
|
||||
//
|
||||
// It allows rendering a custom type which can be marshaled to JSON, YAML, XML,
|
||||
// while also supporting plain text by implementing fmt.Stringer or io.WriterTo.
|
||||
// Binary output is also supported by implementing the encoding.BinaryMarshaler
|
||||
// interface.
|
||||
// It is designed around using a custom type/struct to render your output.
|
||||
// Thanks to Go's marshaling interfaces, you get JSON, YAML, and XML support
|
||||
// almost for free. While plain text output is supported by the type
|
||||
// implementing io.Reader, io.WriterTo, fmt.Stringer, or error interfaces, or by
|
||||
// simply being a type which can easily be type cast to a byte slice.
|
||||
//
|
||||
// Originally intended to easily implement CLI tools which can output their data
|
||||
// as plain text, as well as JSON/YAML with a simple switch of a format string.
|
||||
// But it can just as easily render to any io.Writer.
|
||||
//
|
||||
// The package is designed to be flexible and extensible with a sensible set of
|
||||
// defaults accessible via package level functions. You can create your own
|
||||
// Renderer for custom formats, or create new handlers that support custom
|
||||
// formats.
|
||||
package render
|
||||
|
||||
import (
|
||||
@@ -26,36 +32,11 @@ var (
|
||||
// due to the value not supporting the format, or the value itself not being
|
||||
// renderable. Only Renderer implementations should return this error.
|
||||
ErrCannotRender = fmt.Errorf("%w: cannot render", Err)
|
||||
)
|
||||
|
||||
// FormatRenderer interface is for single format renderers, which can only
|
||||
// render a single format.
|
||||
type FormatRenderer interface {
|
||||
// Render writes v into w in the format that the FormatRenderer supports.
|
||||
//
|
||||
// If v does not implement a required interface, or otherwise cannot be
|
||||
// rendered to the format in question, then a ErrCannotRender error must be
|
||||
// returned. Any other errors should be returned as is.
|
||||
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 (
|
||||
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{
|
||||
// Base is a renderer that supports all formats. It is used by the package
|
||||
// level NewWith function to create new renderers with a sub-set of
|
||||
// formats.
|
||||
Base = New(map[string]Handler{
|
||||
"binary": &Binary{},
|
||||
"json": &JSON{},
|
||||
"text": &Text{},
|
||||
@@ -63,110 +44,39 @@ var (
|
||||
"yaml": &YAML{},
|
||||
})
|
||||
|
||||
DefaultPretty = prettyRenderer.OnlyWith("json", "text", "xml", "yaml")
|
||||
DefaultCompact = compactRenderer.OnlyWith("json", "text", "xml", "yaml")
|
||||
// Default is the default renderer that is used by package level Render,
|
||||
// Compact, Pretty functions. It supports JSON, Text, and YAML formats.
|
||||
Default = Base.NewWith("json", "text", "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.
|
||||
// Render renders the given value to the given writer using the given format. If
|
||||
// pretty is true, the value will be rendered "pretty" if the target format
|
||||
// supports it, otherwise it will be rendered in a compact way.
|
||||
//
|
||||
// By default it supports the following formats:
|
||||
// It uses the default renderer to render the value, which supports JSON, Text,
|
||||
// and YAML formats out of the box.
|
||||
//
|
||||
// - "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.
|
||||
// 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 Render(w io.Writer, format string, pretty bool, v any) error {
|
||||
if pretty {
|
||||
return DefaultPretty.Render(w, format, v)
|
||||
return Default.Render(w, format, pretty, 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 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 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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// 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.
|
||||
// Compact is a convenience function that calls the Default renderer's Compact
|
||||
// method. It is the same as calling Render with pretty set to false.
|
||||
func Compact(w io.Writer, format string, v any) error {
|
||||
return DefaultCompact.Render(w, format, v)
|
||||
return Default.Compact(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)
|
||||
// Pretty is a convenience function that calls the Default renderer's Pretty
|
||||
// method. It is the same as calling Render with pretty set to true.
|
||||
func Pretty(w io.Writer, format string, v any) error {
|
||||
return Default.Pretty(w, format, v)
|
||||
}
|
||||
|
||||
for _, format := range formats {
|
||||
if _, ok := compactRenderer.Renderers[format]; !ok {
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnsupportedFormat, format)
|
||||
}
|
||||
}
|
||||
|
||||
return compactRenderer.OnlyWith(formats...), nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
for _, format := range formats {
|
||||
if _, ok := prettyRenderer.Renderers[format]; !ok {
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnsupportedFormat, format)
|
||||
}
|
||||
}
|
||||
|
||||
return prettyRenderer.OnlyWith(formats...), nil
|
||||
// NewWith creates a new Renderer with the given formats. Only formats on the
|
||||
// BaseRender will be supported.
|
||||
func NewWith(formats ...string) *Renderer {
|
||||
return Base.NewWith(formats...)
|
||||
}
|
||||
|
||||
@@ -39,10 +39,7 @@ func ExampleRender_json() {
|
||||
buf := &bytes.Buffer{}
|
||||
err := render.Pretty(buf, "json", data)
|
||||
if err != nil {
|
||||
fmt.Printf("err: %s\n", err)
|
||||
|
||||
return
|
||||
// panic(err)
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(buf.String())
|
||||
|
||||
@@ -142,13 +139,13 @@ func ExampleRender_xml() {
|
||||
Tags: []string{"golang", "json", "yaml", "toml"},
|
||||
}
|
||||
|
||||
// Create a new renderer that supports XML in addition to default JSON, YAML
|
||||
// and Text.
|
||||
r, _ := render.NewPretty("json", "text", "xml", "yaml")
|
||||
// Create a new renderer that supports XML in addition to the default JSON,
|
||||
// Text, and YAML formats.
|
||||
renderer := render.NewWith("json", "text", "xml", "yaml")
|
||||
|
||||
// Render the object to XML.
|
||||
buf := &bytes.Buffer{}
|
||||
err := r.Render(buf, "xml", data)
|
||||
err := renderer.Pretty(buf, "xml", data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
262
render_test.go
262
render_test.go
@@ -30,23 +30,93 @@ func (mw *mockWriter) String() string {
|
||||
return mw.buf.String()
|
||||
}
|
||||
|
||||
type mockRenderer struct {
|
||||
type mockHandler struct {
|
||||
output string
|
||||
formats []string
|
||||
err error
|
||||
}
|
||||
|
||||
var _ FormatRenderer = (*mockRenderer)(nil)
|
||||
var (
|
||||
_ Handler = (*mockHandler)(nil)
|
||||
_ FormatsHandler = (*mockHandler)(nil)
|
||||
)
|
||||
|
||||
func (m *mockRenderer) Render(w io.Writer, _ any) error {
|
||||
_, err := w.Write([]byte(m.output))
|
||||
func (mh *mockHandler) Render(w io.Writer, _ any) error {
|
||||
_, err := w.Write([]byte(mh.output))
|
||||
|
||||
if m.err != nil {
|
||||
return m.err
|
||||
if mh.err != nil {
|
||||
return mh.err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (mh *mockHandler) Formats() []string {
|
||||
return mh.formats
|
||||
}
|
||||
|
||||
type mockPrettyHandler struct {
|
||||
output string
|
||||
prettyOutput string
|
||||
formats []string
|
||||
err error
|
||||
}
|
||||
|
||||
var (
|
||||
_ Handler = (*mockPrettyHandler)(nil)
|
||||
_ PrettyHandler = (*mockPrettyHandler)(nil)
|
||||
_ FormatsHandler = (*mockPrettyHandler)(nil)
|
||||
)
|
||||
|
||||
func (mph *mockPrettyHandler) Render(w io.Writer, _ any) error {
|
||||
_, err := w.Write([]byte(mph.output))
|
||||
|
||||
if mph.err != nil {
|
||||
return mph.err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (mph *mockPrettyHandler) RenderPretty(w io.Writer, _ any) error {
|
||||
_, err := w.Write([]byte(mph.prettyOutput))
|
||||
|
||||
if mph.err != nil {
|
||||
return mph.err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (mph *mockPrettyHandler) Formats() []string {
|
||||
return mph.formats
|
||||
}
|
||||
|
||||
type mockFormatsHandler struct {
|
||||
output string
|
||||
formats []string
|
||||
err error
|
||||
}
|
||||
|
||||
var (
|
||||
_ Handler = (*mockFormatsHandler)(nil)
|
||||
_ FormatsHandler = (*mockFormatsHandler)(nil)
|
||||
)
|
||||
|
||||
func (mph *mockFormatsHandler) Render(w io.Writer, _ any) error {
|
||||
_, err := w.Write([]byte(mph.output))
|
||||
|
||||
if mph.err != nil {
|
||||
return mph.err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (mph *mockFormatsHandler) Formats() []string {
|
||||
return mph.formats
|
||||
}
|
||||
|
||||
type renderFormatTestCase struct {
|
||||
name string
|
||||
writeErr error
|
||||
@@ -69,6 +139,12 @@ var binaryFormattestCases = []renderFormatTestCase{
|
||||
value: &mockBinaryMarshaler{data: []byte("test string")},
|
||||
want: "test string",
|
||||
},
|
||||
{
|
||||
name: "capitalized format",
|
||||
formats: []string{"BINARY", "BIN"},
|
||||
value: &mockBinaryMarshaler{data: []byte("test string")},
|
||||
want: "test string",
|
||||
},
|
||||
{
|
||||
name: "without binary marshaler",
|
||||
formats: []string{"binary", "bin"},
|
||||
@@ -112,6 +188,13 @@ var jsonFormatTestCases = []renderFormatTestCase{
|
||||
wantPretty: "{\n \"age\": 30\n}\n",
|
||||
wantCompact: "{\"age\":30}\n",
|
||||
},
|
||||
{
|
||||
name: "capitalized format",
|
||||
formats: []string{"JSON"},
|
||||
value: map[string]int{"age": 30},
|
||||
wantPretty: "{\n \"age\": 30\n}\n",
|
||||
wantCompact: "{\"age\":30}\n",
|
||||
},
|
||||
{
|
||||
name: "with json marshaler",
|
||||
formats: []string{"json"},
|
||||
@@ -151,6 +234,12 @@ var textFormatTestCases = []renderFormatTestCase{
|
||||
wantErr: "render: unsupported format: {{format}}",
|
||||
wantErrIs: []error{Err, ErrUnsupportedFormat},
|
||||
},
|
||||
{
|
||||
name: "capitalized format",
|
||||
formats: []string{"TEXT", "TXT", "PLAIN"},
|
||||
value: []byte("test byte slice"),
|
||||
want: "test byte slice",
|
||||
},
|
||||
{
|
||||
name: "byte slice",
|
||||
formats: []string{"text", "txt", "plain"},
|
||||
@@ -344,6 +433,12 @@ var xmlFormatTestCases = []renderFormatTestCase{
|
||||
value: &mockXMLMarshaler{elm: "test string"},
|
||||
want: "<mockXMLMarshaler>test string</mockXMLMarshaler>",
|
||||
},
|
||||
{
|
||||
name: "capitalized format",
|
||||
formats: []string{"XML"},
|
||||
value: &mockXMLMarshaler{elm: "test string"},
|
||||
want: "<mockXMLMarshaler>test string</mockXMLMarshaler>",
|
||||
},
|
||||
{
|
||||
name: "xml format with error from xml.Marshaler",
|
||||
formats: []string{"xml"},
|
||||
@@ -379,6 +474,12 @@ var yamlFormatTestCases = []renderFormatTestCase{
|
||||
value: map[string]int{"age": 30},
|
||||
want: "age: 30\n",
|
||||
},
|
||||
{
|
||||
name: "capitalized format",
|
||||
formats: []string{"YAML", "YML"},
|
||||
value: map[string]int{"age": 30},
|
||||
want: "age: 30\n",
|
||||
},
|
||||
{
|
||||
name: "yaml format with nested structure",
|
||||
formats: []string{"yaml", "yml"},
|
||||
@@ -419,6 +520,72 @@ var yamlFormatTestCases = []renderFormatTestCase{
|
||||
},
|
||||
}
|
||||
|
||||
func TestRender(t *testing.T) {
|
||||
tests := []renderFormatTestCase{}
|
||||
tests = append(tests, jsonFormatTestCases...)
|
||||
tests = append(tests, textFormatTestCases...)
|
||||
tests = append(tests, yamlFormatTestCases...)
|
||||
|
||||
for _, tt := range tests {
|
||||
for _, pretty := range []bool{false, true} {
|
||||
for _, format := range tt.formats {
|
||||
name := format + " format " + tt.name
|
||||
if pretty {
|
||||
name = "pretty " + name
|
||||
}
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
w := &mockWriter{WriteErr: tt.writeErr}
|
||||
|
||||
value := tt.value
|
||||
if tt.valueFunc != nil {
|
||||
value = tt.valueFunc()
|
||||
}
|
||||
|
||||
var err error
|
||||
var panicRes any
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
panicRes = r
|
||||
}
|
||||
}()
|
||||
err = Render(w, format, pretty, value)
|
||||
}()
|
||||
|
||||
got := w.String()
|
||||
want := tt.want
|
||||
if pretty && tt.wantPretty != "" {
|
||||
want = tt.wantPretty
|
||||
} else if tt.wantCompact != "" {
|
||||
want = tt.wantCompact
|
||||
}
|
||||
|
||||
if tt.wantPanic != "" {
|
||||
assert.Equal(t, tt.wantPanic, panicRes)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPretty(t *testing.T) {
|
||||
tests := []renderFormatTestCase{}
|
||||
tests = append(tests, jsonFormatTestCases...)
|
||||
@@ -536,3 +703,86 @@ func TestCompact(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewWith(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
formats []string
|
||||
want *Renderer
|
||||
}{
|
||||
{
|
||||
name: "no formats",
|
||||
formats: nil,
|
||||
want: &Renderer{
|
||||
Handlers: map[string]Handler{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single format",
|
||||
formats: []string{"json"},
|
||||
want: &Renderer{
|
||||
Handlers: map[string]Handler{
|
||||
"json": &JSON{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple formats",
|
||||
formats: []string{"json", "xml"},
|
||||
want: &Renderer{
|
||||
Handlers: map[string]Handler{
|
||||
"json": &JSON{},
|
||||
"xml": &XML{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple formats with aliases",
|
||||
formats: []string{"yaml", "text", "binary"},
|
||||
want: &Renderer{
|
||||
Handlers: map[string]Handler{
|
||||
"bin": &Binary{},
|
||||
"binary": &Binary{},
|
||||
"plain": &Text{},
|
||||
"text": &Text{},
|
||||
"txt": &Text{},
|
||||
"yaml": &YAML{},
|
||||
"yml": &YAML{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "duplicate formats",
|
||||
formats: []string{"json", "json", "yaml", "yaml"},
|
||||
want: &Renderer{
|
||||
Handlers: map[string]Handler{
|
||||
"json": &JSON{},
|
||||
"yaml": &YAML{},
|
||||
"yml": &YAML{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "capitalized formats",
|
||||
formats: []string{"YAML", "TEXT", "BINARY"},
|
||||
want: &Renderer{
|
||||
Handlers: map[string]Handler{
|
||||
"bin": &Binary{},
|
||||
"binary": &Binary{},
|
||||
"plain": &Text{},
|
||||
"text": &Text{},
|
||||
"txt": &Text{},
|
||||
"yaml": &YAML{},
|
||||
"yml": &YAML{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := NewWith(tt.formats...)
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
106
renderer.go
106
renderer.go
@@ -4,61 +4,86 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ErrUnsupportedFormat is returned when a format is not supported by a
|
||||
// renderer. Any method that accepts a format string may return this error.
|
||||
// ErrUnsupportedFormat is returned when a format is not supported by any
|
||||
// Handler.
|
||||
var ErrUnsupportedFormat = fmt.Errorf("%w: unsupported format", Err)
|
||||
|
||||
// Renderer is a renderer that delegates rendering to another renderer
|
||||
// based on a format value.
|
||||
// Renderer exposes methods for rendering values to different formats. The
|
||||
// Renderer delegates rendering to format specific handlers based on the format
|
||||
// string given.
|
||||
type Renderer struct {
|
||||
// Renderers is a map of format names to renderers. When Render is called,
|
||||
// the format is used to look up the renderer to use.
|
||||
Renderers map[string]FormatRenderer
|
||||
// Handlers is a map of format names to Handler. When Render is called,
|
||||
// the format is used to look up the Handler to use.
|
||||
Handlers map[string]Handler
|
||||
}
|
||||
|
||||
// New returns a new Renderer that delegates rendering to the specified
|
||||
// renderers.
|
||||
// Handlers.
|
||||
//
|
||||
// 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))
|
||||
// Any Handlers which implement the FormatsHandler interface, will also be set
|
||||
// as the handler for all format strings returned by Formats() on the handler.
|
||||
func New(handlers map[string]Handler) *Renderer {
|
||||
r := &Renderer{Handlers: make(map[string]Handler, len(handlers))}
|
||||
|
||||
for format, r := range renderers {
|
||||
newRenderers[format] = r
|
||||
for format, handler := range handlers {
|
||||
r.Add(format, handler)
|
||||
}
|
||||
|
||||
if x, ok := r.(Formats); ok {
|
||||
return r
|
||||
}
|
||||
|
||||
// Add adds a Handler to the Renderer. If the handler implements the
|
||||
// FormatsHandler interface, the handler will be added for all formats returned
|
||||
// by Formats().
|
||||
func (r *Renderer) Add(format string, handler Handler) {
|
||||
if format != "" {
|
||||
r.Handlers[strings.ToLower(format)] = handler
|
||||
}
|
||||
|
||||
if x, ok := handler.(FormatsHandler); ok {
|
||||
for _, f := range x.Formats() {
|
||||
if f != format {
|
||||
newRenderers[f] = r
|
||||
if f != "" && f != format {
|
||||
r.Handlers[strings.ToLower(f)] = handler
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &Renderer{Renderers: newRenderers}
|
||||
}
|
||||
|
||||
// Render renders a value to an io.Writer using the specified format. If the
|
||||
// format is not supported, ErrUnsupportedFormat is returned.
|
||||
// Render renders a value to the given io.Writer using the specified format.
|
||||
//
|
||||
// If the format is supported, but the value cannot be rendered to the format,
|
||||
// the error returned by the renderer is returned. In most cases this will be
|
||||
// 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.Renderers[format]
|
||||
// If pretty is true, it will attempt to render the value with pretty
|
||||
// formatting if the underlying Handler supports pretty formatting.
|
||||
//
|
||||
// If the format is not supported or the value cannot be rendered to the format,
|
||||
// a ErrUnsupportedFormat error is returned.
|
||||
func (r *Renderer) Render(
|
||||
w io.Writer,
|
||||
format string,
|
||||
pretty bool,
|
||||
v any,
|
||||
) error {
|
||||
handler, ok := r.Handlers[strings.ToLower(format)]
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: %s", ErrUnsupportedFormat, format)
|
||||
}
|
||||
|
||||
err := renderer.Render(w, v)
|
||||
prettyHandler, ok := handler.(PrettyHandler)
|
||||
var err error
|
||||
if pretty && ok {
|
||||
err = prettyHandler.RenderPretty(w, v)
|
||||
} else {
|
||||
err = handler.Render(w, v)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrCannotRender) {
|
||||
return fmt.Errorf("%w: %s", ErrUnsupportedFormat, format)
|
||||
}
|
||||
|
||||
// Ensure that the error is wrapped with ErrFailed if it is not already.
|
||||
if !errors.Is(err, ErrFailed) {
|
||||
return fmt.Errorf("%w: %w", ErrFailed, err)
|
||||
}
|
||||
@@ -69,14 +94,27 @@ 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))
|
||||
// Compact is a convenience method that calls Render with pretty set to false.
|
||||
func (r *Renderer) Compact(w io.Writer, format string, v any) error {
|
||||
return r.Render(w, format, false, v)
|
||||
}
|
||||
|
||||
// Pretty is a convenience method that calls Render with pretty set to true.
|
||||
func (r *Renderer) Pretty(w io.Writer, format string, v any) error {
|
||||
return r.Render(w, format, true, v)
|
||||
}
|
||||
|
||||
// NewWith creates a new Renderer with the formats given, if they have handlers
|
||||
// in the currener Renderer. It essentially allows to restrict a Renderer to a
|
||||
// only a sub-set of supported formats.
|
||||
func (r *Renderer) NewWith(formats ...string) *Renderer {
|
||||
handlers := make(map[string]Handler, len(formats))
|
||||
|
||||
for _, format := range formats {
|
||||
if r, ok := r.Renderers[format]; ok {
|
||||
renderers[format] = r
|
||||
if r, ok := r.Handlers[strings.ToLower(format)]; ok {
|
||||
handlers[format] = r
|
||||
}
|
||||
}
|
||||
|
||||
return New(renderers)
|
||||
return New(handlers)
|
||||
}
|
||||
|
||||
551
renderer_test.go
551
renderer_test.go
@@ -8,32 +8,230 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
handlers map[string]Handler
|
||||
want *Renderer
|
||||
}{
|
||||
{
|
||||
name: "nil handlers map",
|
||||
want: &Renderer{Handlers: map[string]Handler{}},
|
||||
},
|
||||
{
|
||||
name: "empty handlers map",
|
||||
handlers: map[string]Handler{},
|
||||
want: &Renderer{Handlers: map[string]Handler{}},
|
||||
},
|
||||
{
|
||||
name: "single handler",
|
||||
handlers: map[string]Handler{
|
||||
"mock": &mockHandler{},
|
||||
},
|
||||
want: &Renderer{Handlers: map[string]Handler{
|
||||
"mock": &mockHandler{},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "multiple handlers",
|
||||
handlers: map[string]Handler{
|
||||
"mock": &mockHandler{},
|
||||
"other": &mockHandler{output: "other output"},
|
||||
},
|
||||
want: &Renderer{Handlers: map[string]Handler{
|
||||
"mock": &mockHandler{},
|
||||
"other": &mockHandler{output: "other output"},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "multiple handlers with alias formats",
|
||||
handlers: map[string]Handler{
|
||||
"mock": &mockFormatsHandler{formats: []string{"mock", "m"}},
|
||||
"other": &mockFormatsHandler{formats: []string{"other", "o"}},
|
||||
},
|
||||
want: &Renderer{Handlers: map[string]Handler{
|
||||
"mock": &mockFormatsHandler{formats: []string{"mock", "m"}},
|
||||
"m": &mockFormatsHandler{formats: []string{"mock", "m"}},
|
||||
"other": &mockFormatsHandler{formats: []string{"other", "o"}},
|
||||
"o": &mockFormatsHandler{formats: []string{"other", "o"}},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "multiple handlers with custom formats",
|
||||
handlers: map[string]Handler{
|
||||
"foo": &mockFormatsHandler{formats: []string{"mock", "m"}},
|
||||
"bar": &mockFormatsHandler{formats: []string{"other", "o"}},
|
||||
},
|
||||
want: &Renderer{Handlers: map[string]Handler{
|
||||
"foo": &mockFormatsHandler{formats: []string{"mock", "m"}},
|
||||
"mock": &mockFormatsHandler{formats: []string{"mock", "m"}},
|
||||
"m": &mockFormatsHandler{formats: []string{"mock", "m"}},
|
||||
"bar": &mockFormatsHandler{formats: []string{"other", "o"}},
|
||||
"other": &mockFormatsHandler{formats: []string{"other", "o"}},
|
||||
"o": &mockFormatsHandler{formats: []string{"other", "o"}},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "multiple handlers with capitalized formats",
|
||||
handlers: map[string]Handler{
|
||||
"Foo": &mockFormatsHandler{formats: []string{"MOCK", "m"}},
|
||||
"Bar": &mockFormatsHandler{formats: []string{"OTHER", "o"}},
|
||||
},
|
||||
want: &Renderer{Handlers: map[string]Handler{
|
||||
"foo": &mockFormatsHandler{formats: []string{"MOCK", "m"}},
|
||||
"mock": &mockFormatsHandler{formats: []string{"MOCK", "m"}},
|
||||
"m": &mockFormatsHandler{formats: []string{"MOCK", "m"}},
|
||||
"bar": &mockFormatsHandler{formats: []string{"OTHER", "o"}},
|
||||
"other": &mockFormatsHandler{formats: []string{"OTHER", "o"}},
|
||||
"o": &mockFormatsHandler{formats: []string{"OTHER", "o"}},
|
||||
}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := New(tt.handlers)
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderer_Add(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
format string
|
||||
handler Handler
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "handler without Formats",
|
||||
format: "tackle",
|
||||
handler: &mockHandler{},
|
||||
want: []string{"tackle"},
|
||||
},
|
||||
{
|
||||
name: "hander with Formats",
|
||||
format: "hackle",
|
||||
handler: &mockFormatsHandler{formats: []string{"hackle"}},
|
||||
want: []string{"hackle"},
|
||||
},
|
||||
{
|
||||
name: "hander with alias formats",
|
||||
format: "hackle",
|
||||
handler: &mockFormatsHandler{formats: []string{"hackle", "hack"}},
|
||||
want: []string{"hackle", "hack"},
|
||||
},
|
||||
{
|
||||
name: "given format differs from Formats",
|
||||
format: "foobar",
|
||||
handler: &mockFormatsHandler{formats: []string{"hackle", "hack"}},
|
||||
want: []string{"foobar", "hackle", "hack"},
|
||||
},
|
||||
{
|
||||
name: "lowercases capitalized formats",
|
||||
format: "FooBar",
|
||||
handler: &mockFormatsHandler{formats: []string{"HACKLE", "Hack"}},
|
||||
want: []string{"foobar", "hackle", "hack"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &Renderer{Handlers: map[string]Handler{}}
|
||||
|
||||
r.Add(tt.format, tt.handler)
|
||||
|
||||
for _, f := range tt.want {
|
||||
got, ok := r.Handlers[f]
|
||||
assert.Truef(t, ok, "not added as %q format", f)
|
||||
assert.Equal(t, tt.handler, got)
|
||||
}
|
||||
|
||||
gotFormats := []string{}
|
||||
for f := range r.Handlers {
|
||||
gotFormats = append(gotFormats, f)
|
||||
}
|
||||
assert.ElementsMatch(t, tt.want, gotFormats)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderer_Render(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
renderers map[string]FormatRenderer
|
||||
handlers map[string]Handler
|
||||
format string
|
||||
value interface{}
|
||||
pretty bool
|
||||
value any
|
||||
want string
|
||||
wantErr string
|
||||
wantErrIs []error
|
||||
}{
|
||||
{
|
||||
name: "existing renderer",
|
||||
renderers: map[string]FormatRenderer{
|
||||
"mock": &mockRenderer{output: "mock output"},
|
||||
name: "no pretty with handler that supports pretty",
|
||||
handlers: map[string]Handler{
|
||||
"mock": &mockPrettyHandler{
|
||||
output: "plain output",
|
||||
prettyOutput: "pretty output",
|
||||
},
|
||||
},
|
||||
format: "mock",
|
||||
pretty: false,
|
||||
value: struct{}{},
|
||||
want: "mock output",
|
||||
want: "plain output",
|
||||
},
|
||||
{
|
||||
name: "existing renderer returns error",
|
||||
renderers: map[string]FormatRenderer{
|
||||
"other": &mockRenderer{
|
||||
name: "no pretty with handler that does not support pretty",
|
||||
handlers: map[string]Handler{
|
||||
"mock": &mockHandler{output: "plain output"},
|
||||
},
|
||||
format: "mock",
|
||||
pretty: false,
|
||||
value: struct{}{},
|
||||
want: "plain output",
|
||||
},
|
||||
{
|
||||
name: "pretty with handler that supports pretty",
|
||||
handlers: map[string]Handler{
|
||||
"mock": &mockPrettyHandler{
|
||||
output: "plain output",
|
||||
prettyOutput: "pretty output",
|
||||
},
|
||||
},
|
||||
format: "mock",
|
||||
pretty: true,
|
||||
value: struct{}{},
|
||||
want: "pretty output",
|
||||
},
|
||||
{
|
||||
name: "pretty with handler that does not support pretty",
|
||||
handlers: map[string]Handler{
|
||||
"mock": &mockHandler{
|
||||
output: "plain output",
|
||||
},
|
||||
},
|
||||
format: "mock",
|
||||
pretty: true,
|
||||
value: struct{}{},
|
||||
want: "plain output",
|
||||
},
|
||||
{
|
||||
name: "normalizes given format to lowercase",
|
||||
handlers: map[string]Handler{
|
||||
"mock": &mockHandler{
|
||||
output: "plain output",
|
||||
},
|
||||
},
|
||||
format: "MOCK",
|
||||
pretty: true,
|
||||
value: struct{}{},
|
||||
want: "plain output",
|
||||
},
|
||||
{
|
||||
name: "handler returns error",
|
||||
handlers: map[string]Handler{
|
||||
"other": &mockHandler{
|
||||
output: "mock output",
|
||||
err: errors.New("mock error"),
|
||||
},
|
||||
@@ -43,9 +241,9 @@ func TestRenderer_Render(t *testing.T) {
|
||||
wantErr: "render: failed: mock error",
|
||||
},
|
||||
{
|
||||
name: "existing renderer returns ErrCannotRender",
|
||||
renderers: map[string]FormatRenderer{
|
||||
"other": &mockRenderer{
|
||||
name: "handler returns ErrCannotRender",
|
||||
handlers: map[string]Handler{
|
||||
"other": &mockHandler{
|
||||
output: "mock output",
|
||||
err: fmt.Errorf("%w: mock", ErrCannotRender),
|
||||
},
|
||||
@@ -56,8 +254,8 @@ func TestRenderer_Render(t *testing.T) {
|
||||
wantErrIs: []error{Err, ErrUnsupportedFormat},
|
||||
},
|
||||
{
|
||||
name: "non-existing renderer",
|
||||
renderers: map[string]FormatRenderer{},
|
||||
name: "non-existing handler",
|
||||
handlers: map[string]Handler{},
|
||||
format: "unknown",
|
||||
value: struct{}{},
|
||||
wantErr: "render: unsupported format: unknown",
|
||||
@@ -66,12 +264,194 @@ func TestRenderer_Render(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fr := &Renderer{
|
||||
Renderers: tt.renderers,
|
||||
r := &Renderer{
|
||||
Handlers: tt.handlers,
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
|
||||
err := fr.Render(&buf, tt.format, tt.value)
|
||||
err := r.Render(&buf, tt.format, tt.pretty, tt.value)
|
||||
got := buf.String()
|
||||
|
||||
if tt.wantErr != "" {
|
||||
assert.EqualError(t, err, tt.wantErr)
|
||||
}
|
||||
for _, e := range tt.wantErrIs {
|
||||
assert.ErrorIs(t, err, e)
|
||||
}
|
||||
|
||||
if tt.wantErr == "" && len(tt.wantErrIs) == 0 {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderer_Compact(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
handlers map[string]Handler
|
||||
format string
|
||||
value any
|
||||
want string
|
||||
wantErr string
|
||||
wantErrIs []error
|
||||
}{
|
||||
{
|
||||
name: "handler supports pretty",
|
||||
handlers: map[string]Handler{
|
||||
"mock": &mockPrettyHandler{
|
||||
output: "plain output",
|
||||
prettyOutput: "pretty output",
|
||||
},
|
||||
},
|
||||
format: "mock",
|
||||
value: struct{}{},
|
||||
want: "plain output",
|
||||
},
|
||||
{
|
||||
name: "handler does not support pretty",
|
||||
handlers: map[string]Handler{
|
||||
"mock": &mockHandler{output: "plain output"},
|
||||
},
|
||||
format: "mock",
|
||||
value: struct{}{},
|
||||
want: "plain output",
|
||||
},
|
||||
{
|
||||
name: "handler returns error",
|
||||
handlers: map[string]Handler{
|
||||
"other": &mockHandler{
|
||||
output: "mock output",
|
||||
err: errors.New("mock error"),
|
||||
},
|
||||
},
|
||||
format: "other",
|
||||
value: struct{}{},
|
||||
wantErr: "render: failed: mock error",
|
||||
},
|
||||
{
|
||||
name: "handler returns ErrCannotRender",
|
||||
handlers: map[string]Handler{
|
||||
"other": &mockHandler{
|
||||
output: "mock output",
|
||||
err: fmt.Errorf("%w: mock", ErrCannotRender),
|
||||
},
|
||||
},
|
||||
format: "other",
|
||||
value: struct{}{},
|
||||
wantErr: "render: unsupported format: other",
|
||||
wantErrIs: []error{Err, ErrUnsupportedFormat},
|
||||
},
|
||||
{
|
||||
name: "non-existing handler",
|
||||
handlers: map[string]Handler{},
|
||||
format: "unknown",
|
||||
value: struct{}{},
|
||||
wantErr: "render: unsupported format: unknown",
|
||||
wantErrIs: []error{Err, ErrUnsupportedFormat},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &Renderer{
|
||||
Handlers: tt.handlers,
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
|
||||
err := r.Compact(&buf, tt.format, tt.value)
|
||||
got := buf.String()
|
||||
|
||||
if tt.wantErr != "" {
|
||||
assert.EqualError(t, err, tt.wantErr)
|
||||
}
|
||||
for _, e := range tt.wantErrIs {
|
||||
assert.ErrorIs(t, err, e)
|
||||
}
|
||||
|
||||
if tt.wantErr == "" && len(tt.wantErrIs) == 0 {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderer_Pretty(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
handlers map[string]Handler
|
||||
format string
|
||||
value any
|
||||
want string
|
||||
wantErr string
|
||||
wantErrIs []error
|
||||
}{
|
||||
{
|
||||
name: "handler supports pretty",
|
||||
handlers: map[string]Handler{
|
||||
"mock": &mockPrettyHandler{
|
||||
output: "plain output",
|
||||
prettyOutput: "pretty output",
|
||||
},
|
||||
},
|
||||
format: "mock",
|
||||
value: struct{}{},
|
||||
want: "pretty output",
|
||||
},
|
||||
{
|
||||
name: "handler does not support pretty",
|
||||
handlers: map[string]Handler{
|
||||
"mock": &mockHandler{
|
||||
output: "plain output",
|
||||
},
|
||||
},
|
||||
format: "mock",
|
||||
value: struct{}{},
|
||||
want: "plain output",
|
||||
},
|
||||
{
|
||||
name: "handler returns error",
|
||||
handlers: map[string]Handler{
|
||||
"other": &mockHandler{
|
||||
output: "mock output",
|
||||
err: errors.New("mock error"),
|
||||
},
|
||||
},
|
||||
format: "other",
|
||||
value: struct{}{},
|
||||
wantErr: "render: failed: mock error",
|
||||
},
|
||||
{
|
||||
name: "handler returns ErrCannotRender",
|
||||
handlers: map[string]Handler{
|
||||
"other": &mockHandler{
|
||||
output: "mock output",
|
||||
err: fmt.Errorf("%w: mock", ErrCannotRender),
|
||||
},
|
||||
},
|
||||
format: "other",
|
||||
value: struct{}{},
|
||||
wantErr: "render: unsupported format: other",
|
||||
wantErrIs: []error{Err, ErrUnsupportedFormat},
|
||||
},
|
||||
{
|
||||
name: "non-existing handler",
|
||||
handlers: map[string]Handler{},
|
||||
format: "unknown",
|
||||
value: struct{}{},
|
||||
wantErr: "render: unsupported format: unknown",
|
||||
wantErrIs: []error{Err, ErrUnsupportedFormat},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &Renderer{
|
||||
Handlers: tt.handlers,
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
|
||||
err := r.Pretty(&buf, tt.format, tt.value)
|
||||
got := buf.String()
|
||||
|
||||
if tt.wantErr != "" {
|
||||
@@ -97,6 +477,75 @@ func TestRenderer_RenderAllFormats(t *testing.T) {
|
||||
tests = append(tests, xmlFormatTestCases...)
|
||||
tests = append(tests, yamlFormatTestCases...)
|
||||
|
||||
for _, tt := range tests {
|
||||
for _, pretty := range []bool{false, true} {
|
||||
for _, format := range tt.formats {
|
||||
name := format + " format " + tt.name
|
||||
if pretty {
|
||||
name = "pretty " + name
|
||||
}
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
w := &mockWriter{WriteErr: tt.writeErr}
|
||||
|
||||
value := tt.value
|
||||
if tt.valueFunc != nil {
|
||||
value = tt.valueFunc()
|
||||
}
|
||||
|
||||
var err error
|
||||
var panicRes any
|
||||
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
panicRes = r
|
||||
}
|
||||
}()
|
||||
err = Base.Render(w, format, pretty, value)
|
||||
}()
|
||||
|
||||
got := w.String()
|
||||
want := tt.want
|
||||
if pretty && tt.wantPretty != "" {
|
||||
want = tt.wantPretty
|
||||
} else if tt.wantCompact != "" {
|
||||
want = tt.wantCompact
|
||||
}
|
||||
|
||||
if tt.wantPanic != "" {
|
||||
assert.Equal(t, tt.wantPanic, panicRes)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderer_CompactAllFormats(t *testing.T) {
|
||||
tests := []renderFormatTestCase{}
|
||||
tests = append(tests, binaryFormattestCases...)
|
||||
tests = append(tests, jsonFormatTestCases...)
|
||||
tests = append(tests, textFormatTestCases...)
|
||||
tests = append(tests, xmlFormatTestCases...)
|
||||
tests = append(tests, yamlFormatTestCases...)
|
||||
|
||||
for _, tt := range tests {
|
||||
for _, format := range tt.formats {
|
||||
t.Run(format+" format "+tt.name, func(t *testing.T) {
|
||||
@@ -109,8 +558,6 @@ func TestRenderer_RenderAllFormats(t *testing.T) {
|
||||
|
||||
var err error
|
||||
var panicRes any
|
||||
renderer := compactRenderer
|
||||
require.NoError(t, err)
|
||||
|
||||
func() {
|
||||
defer func() {
|
||||
@@ -118,7 +565,7 @@ func TestRenderer_RenderAllFormats(t *testing.T) {
|
||||
panicRes = r
|
||||
}
|
||||
}()
|
||||
err = renderer.Render(w, format, value)
|
||||
err = Base.Compact(w, format, value)
|
||||
}()
|
||||
|
||||
got := w.String()
|
||||
@@ -152,3 +599,65 @@ func TestRenderer_RenderAllFormats(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderer_PrettyAllFormats(t *testing.T) {
|
||||
tests := []renderFormatTestCase{}
|
||||
tests = append(tests, binaryFormattestCases...)
|
||||
tests = append(tests, jsonFormatTestCases...)
|
||||
tests = append(tests, textFormatTestCases...)
|
||||
tests = append(tests, xmlFormatTestCases...)
|
||||
tests = append(tests, yamlFormatTestCases...)
|
||||
|
||||
for _, tt := range tests {
|
||||
for _, format := range tt.formats {
|
||||
t.Run(format+" format "+tt.name, func(t *testing.T) {
|
||||
w := &mockWriter{WriteErr: tt.writeErr}
|
||||
|
||||
value := tt.value
|
||||
if tt.valueFunc != nil {
|
||||
value = tt.valueFunc()
|
||||
}
|
||||
|
||||
var err error
|
||||
var panicRes any
|
||||
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
panicRes = r
|
||||
}
|
||||
}()
|
||||
err = Base.Pretty(w, format, value)
|
||||
}()
|
||||
|
||||
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.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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
text.go
12
text.go
@@ -5,8 +5,10 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// Text is a renderer that writes the given value to the writer as is. It
|
||||
// supports rendering the following types as plain text:
|
||||
// Text is a Handler that writes the given value to the writer as text,
|
||||
// supporting multiple types and interfaces.
|
||||
//
|
||||
// Supports rendering the following types as text:
|
||||
//
|
||||
// - []byte
|
||||
// - []rune
|
||||
@@ -23,7 +25,10 @@ import (
|
||||
// If the value is of any other type, a ErrCannotRender error will be returned.
|
||||
type Text struct{}
|
||||
|
||||
var _ FormatRenderer = (*Text)(nil)
|
||||
var (
|
||||
_ Handler = (*Text)(nil)
|
||||
_ FormatsHandler = (*Text)(nil)
|
||||
)
|
||||
|
||||
// Render writes the given value to the writer as text.
|
||||
func (t *Text) Render(w io.Writer, v any) error {
|
||||
@@ -58,6 +63,7 @@ func (t *Text) Render(w io.Writer, v any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Formats returns a list of format strings that this Handler supports.
|
||||
func (t *Text) Formats() []string {
|
||||
return []string{"text", "txt", "plain"}
|
||||
}
|
||||
|
||||
@@ -216,3 +216,9 @@ func TestText_Render(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestText_Formats(t *testing.T) {
|
||||
h := &Text{}
|
||||
|
||||
assert.Equal(t, []string{"text", "txt", "plain"}, h.Formats())
|
||||
}
|
||||
|
||||
40
xml.go
40
xml.go
@@ -6,35 +6,48 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// XMLDefualtIndent is the default indentation string used by XML instances when
|
||||
// pretty rendering if no Indent value is set.
|
||||
var XMLDefualtIndent = " "
|
||||
|
||||
// XML is a Renderer that marshals a value to XML.
|
||||
type XML struct {
|
||||
// Pretty specifies whether the output should be pretty-printed. If true,
|
||||
// the output will be indented and newlines will be added.
|
||||
Pretty bool
|
||||
|
||||
// Prefix is the prefix added to each level of indentation when Pretty is
|
||||
// true.
|
||||
// Prefix is the prefix added to each level of indentation when pretty
|
||||
// rendering.
|
||||
Prefix string
|
||||
|
||||
// Indent is the string added to each level of indentation when Pretty is
|
||||
// true. If empty, two spaces will be used instead.
|
||||
// Indent is the string added to each level of indentation when pretty
|
||||
// rendering. If empty, XMLDefualtIndent be used.
|
||||
Indent string
|
||||
}
|
||||
|
||||
var _ FormatRenderer = (*XML)(nil)
|
||||
var (
|
||||
_ Handler = (*XML)(nil)
|
||||
_ PrettyHandler = (*XML)(nil)
|
||||
_ FormatsHandler = (*XML)(nil)
|
||||
)
|
||||
|
||||
// Render marshals the given value to XML.
|
||||
func (x *XML) Render(w io.Writer, v any) error {
|
||||
enc := xml.NewEncoder(w)
|
||||
if x.Pretty {
|
||||
err := xml.NewEncoder(w).Encode(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrFailed, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RenderPretty marshals the given value to XML with line breaks and
|
||||
// indentation.
|
||||
func (x *XML) RenderPretty(w io.Writer, v any) error {
|
||||
prefix := x.Prefix
|
||||
indent := x.Indent
|
||||
if indent == "" {
|
||||
indent = " "
|
||||
indent = XMLDefualtIndent
|
||||
}
|
||||
|
||||
enc := xml.NewEncoder(w)
|
||||
enc.Indent(prefix, indent)
|
||||
}
|
||||
|
||||
err := enc.Encode(v)
|
||||
if err != nil {
|
||||
@@ -44,6 +57,7 @@ func (x *XML) Render(w io.Writer, v any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Formats returns a list of format strings that this Handler supports.
|
||||
func (x *XML) Formats() []string {
|
||||
return []string{"xml"}
|
||||
}
|
||||
|
||||
108
xml_test.go
108
xml_test.go
@@ -32,7 +32,6 @@ func (mxm *mockXMLMarshaler) MarshalXML(
|
||||
func TestXML_Render(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pretty bool
|
||||
prefix string
|
||||
indent string
|
||||
value any
|
||||
@@ -41,8 +40,7 @@ func TestXML_Render(t *testing.T) {
|
||||
wantErrIs []error
|
||||
}{
|
||||
{
|
||||
name: "simple object without pretty",
|
||||
pretty: false,
|
||||
name: "simple object",
|
||||
value: struct {
|
||||
XMLName xml.Name `xml:"user"`
|
||||
Age int `xml:"age"`
|
||||
@@ -50,28 +48,7 @@ func TestXML_Render(t *testing.T) {
|
||||
want: `<user><age>30</age></user>`,
|
||||
},
|
||||
{
|
||||
name: "simple object with pretty",
|
||||
pretty: true,
|
||||
value: struct {
|
||||
XMLName xml.Name `xml:"user"`
|
||||
Age int `xml:"age"`
|
||||
}{Age: 30},
|
||||
want: "<user>\n <age>30</age>\n</user>",
|
||||
},
|
||||
{
|
||||
name: "pretty with prefix and indent",
|
||||
pretty: true,
|
||||
prefix: "//",
|
||||
indent: "\t",
|
||||
value: struct {
|
||||
XMLName xml.Name `xml:"user"`
|
||||
Age int `xml:"age"`
|
||||
}{Age: 30},
|
||||
want: "//<user>\n//\t<age>30</age>\n//</user>",
|
||||
},
|
||||
{
|
||||
name: "prefix and indent without pretty",
|
||||
pretty: false,
|
||||
name: "ignores indent without pretty",
|
||||
prefix: "//",
|
||||
indent: "\t",
|
||||
value: struct {
|
||||
@@ -93,7 +70,6 @@ func TestXML_Render(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "invalid value",
|
||||
pretty: false,
|
||||
value: make(chan int),
|
||||
wantErr: "render: failed: xml: unsupported type: chan int",
|
||||
wantErrIs: []error{Err, ErrFailed},
|
||||
@@ -103,7 +79,6 @@ func TestXML_Render(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
x := &XML{
|
||||
Pretty: tt.pretty,
|
||||
Prefix: tt.prefix,
|
||||
Indent: tt.indent,
|
||||
}
|
||||
@@ -126,3 +101,82 @@ func TestXML_Render(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestXML_RenderPretty(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
prefix string
|
||||
indent string
|
||||
value any
|
||||
want string
|
||||
wantErr string
|
||||
wantErrIs []error
|
||||
}{
|
||||
{
|
||||
name: "simple object",
|
||||
value: struct {
|
||||
XMLName xml.Name `xml:"user"`
|
||||
Age int `xml:"age"`
|
||||
}{Age: 30},
|
||||
want: "<user>\n <age>30</age>\n</user>",
|
||||
},
|
||||
{
|
||||
name: "uses prefix and indent",
|
||||
prefix: "//",
|
||||
indent: "\t",
|
||||
value: struct {
|
||||
XMLName xml.Name `xml:"user"`
|
||||
Age int `xml:"age"`
|
||||
}{Age: 30},
|
||||
want: "//<user>\n//\t<age>30</age>\n//</user>",
|
||||
},
|
||||
{
|
||||
name: "implements xml.Marshaler",
|
||||
value: &mockXMLMarshaler{elm: "test string"},
|
||||
want: "<mockXMLMarshaler>test string</mockXMLMarshaler>",
|
||||
},
|
||||
{
|
||||
name: "error from xml.Marshaler",
|
||||
value: &mockXMLMarshaler{err: errors.New("mock error")},
|
||||
wantErr: "render: failed: mock error",
|
||||
wantErrIs: []error{Err, ErrFailed},
|
||||
},
|
||||
{
|
||||
name: "invalid value",
|
||||
value: make(chan int),
|
||||
wantErr: "render: failed: xml: unsupported type: chan int",
|
||||
wantErrIs: []error{Err, ErrFailed},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
x := &XML{
|
||||
Prefix: tt.prefix,
|
||||
Indent: tt.indent,
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
|
||||
err := x.RenderPretty(&buf, tt.value)
|
||||
got := buf.String()
|
||||
|
||||
if tt.wantErr != "" {
|
||||
assert.EqualError(t, err, tt.wantErr)
|
||||
}
|
||||
for _, e := range tt.wantErrIs {
|
||||
assert.ErrorIs(t, err, e)
|
||||
}
|
||||
|
||||
if tt.wantErr == "" && len(tt.wantErrIs) == 0 {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestXML_Formats(t *testing.T) {
|
||||
h := &XML{}
|
||||
|
||||
assert.Equal(t, []string{"xml"}, h.Formats())
|
||||
}
|
||||
|
||||
17
yaml.go
17
yaml.go
@@ -7,24 +7,28 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// YAML is a renderer that marshals the given value to YAML.
|
||||
var YAMLDefaultIndent = 2
|
||||
|
||||
// YAML is a Handler that marshals the given value to YAML.
|
||||
type YAML struct {
|
||||
// Indent controls how many spaces will be used for indenting nested blocks
|
||||
// in the output YAML. When Indent is zero, 2 will be used by default.
|
||||
// in the output YAML. When Indent is zero, YAMLDefaultIndent will be used.
|
||||
Indent int
|
||||
}
|
||||
|
||||
var _ FormatRenderer = (*YAML)(nil)
|
||||
var (
|
||||
_ Handler = (*YAML)(nil)
|
||||
_ FormatsHandler = (*YAML)(nil)
|
||||
)
|
||||
|
||||
// Render marshals the given value to YAML.
|
||||
func (y *YAML) Render(w io.Writer, v any) error {
|
||||
enc := yaml.NewEncoder(w)
|
||||
|
||||
indent := y.Indent
|
||||
if indent == 0 {
|
||||
indent = 2
|
||||
indent = YAMLDefaultIndent
|
||||
}
|
||||
|
||||
enc := yaml.NewEncoder(w)
|
||||
enc.SetIndent(indent)
|
||||
|
||||
err := enc.Encode(v)
|
||||
@@ -35,6 +39,7 @@ func (y *YAML) Render(w io.Writer, v any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Formats returns a list of format strings that this Handler supports.
|
||||
func (y *YAML) Formats() []string {
|
||||
return []string{"yaml", "yml"}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestYAML_Render(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
indent int
|
||||
value interface{}
|
||||
value any
|
||||
want string
|
||||
wantErr string
|
||||
wantErrIs []error
|
||||
@@ -113,3 +113,9 @@ func TestYAML_Render(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestYAML_Formats(t *testing.T) {
|
||||
h := &YAML{}
|
||||
|
||||
assert.Equal(t, []string{"yaml", "yml"}, h.Formats())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user