diff --git a/text.go b/text.go index ebad65b..ca1da49 100644 --- a/text.go +++ b/text.go @@ -5,19 +5,50 @@ 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: +// +// - []byte +// - []rune +// - string +// - int, int8, int16, int32, int64 +// - uint, uint8, uint16, uint32, uint64 +// - float32, float64 +// - bool +// - io.Reader +// - io.WriterTo +// - fmt.Stringer +// - error +// +// If the value is of any other type, a ErrCannotRender error will be returned. type Text struct{} var _ FormatRenderer = (*Text)(nil) +// Render writes the given value to the writer as text. func (t *Text) Render(w io.Writer, v any) error { var err error switch x := v.(type) { - case fmt.Stringer: - _, err = w.Write([]byte(x.String())) + case []byte: + _, err = w.Write(x) + case []rune: + _, err = w.Write([]byte(string(x))) + case string: + _, err = w.Write([]byte(x)) + case int, int8, int16, int32, int64, + uint, uint8, uint16, uint32, uint64, + float32, float64, bool: + _, err = fmt.Fprintf(w, "%v", x) + case io.Reader: + _, err = io.Copy(w, x) case io.WriterTo: _, err = x.WriteTo(w) + case fmt.Stringer: + _, err = w.Write([]byte(x.String())) + case error: + _, err = w.Write([]byte(x.Error())) default: - return ErrCannotRender + return fmt.Errorf("%w: %T", ErrCannotRender, v) } if err != nil { diff --git a/text_test.go b/text_test.go index 46edf9a..063f59e 100644 --- a/text_test.go +++ b/text_test.go @@ -37,6 +37,58 @@ func (m *mockWriterTo) WriteTo(w io.Writer) (int64, error) { return int64(n), err } +type mockReader struct { + value string + cursor int + err error +} + +var _ io.Reader = (*mockReader)(nil) + +func (m *mockReader) Read(p []byte) (n int, err error) { + if m.err != nil { + return 0, m.err + } + + if len(m.value) == 0 { + return 0, io.EOF + } + + n = copy(p, m.value[m.cursor:]) + m.cursor += n + + if m.cursor >= len(m.value) { + return n, io.EOF + } + + return n, nil +} + +func Test_mockReader_Read(t *testing.T) { + mr := &mockReader{value: "test string"} + + b1 := make([]byte, 5) + n1, err := mr.Read(b1) + + assert.NoError(t, err) + assert.Equal(t, 5, n1) + assert.Equal(t, "test ", string(b1)) + + b2 := make([]byte, 5) + n2, err := mr.Read(b2) + + assert.NoError(t, err) + assert.Equal(t, 5, n2) + assert.Equal(t, "strin", string(b2)) + + b3 := make([]byte, 5) + n3, err := mr.Read(b3) + + assert.Equal(t, io.EOF, err) + assert.Equal(t, 1, n3) + assert.Equal(t, []byte{byte('g'), 0, 0, 0, 0}, b3) +} + func TestText_Render(t *testing.T) { tests := []struct { name string @@ -46,6 +98,51 @@ func TestText_Render(t *testing.T) { wantErr string wantErrIs []error }{ + { + name: "nil", + value: nil, + wantErr: "render: cannot render: ", + wantErrIs: []error{render.Err, render.ErrCannotRender}, + }, + { + name: "byte slice", + value: []byte("test byte slice"), + want: "test byte slice", + }, + { + name: "nil byte slice", + value: []byte(nil), + want: "", + }, + { + name: "empty byte slice", + value: []byte{}, + want: "", + }, + { + name: "rune slice", + value: []rune{'r', 'u', 'n', 'e', 's', '!', ' ', 'y', 'e', 's'}, + want: "runes! yes", + }, + { + name: "string", + value: "test string", + want: "test string", + }, + {name: "int", value: int(42), want: "42"}, + {name: "int8", value: int8(43), want: "43"}, + {name: "int16", value: int16(44), want: "44"}, + {name: "int32", value: int32(45), want: "45"}, + {name: "int64", value: int64(46), want: "46"}, + {name: "uint", value: uint(47), want: "47"}, + {name: "uint8", value: uint8(48), want: "48"}, + {name: "uint16", value: uint16(49), want: "49"}, + {name: "uint32", value: uint32(50), want: "50"}, + {name: "uint64", value: uint64(51), want: "51"}, + {name: "float32", value: float32(3.14), want: "3.14"}, + {name: "float64", value: float64(3.14159), want: "3.14159"}, + {name: "bool true", value: true, want: "true"}, + {name: "bool false", value: false, want: "false"}, { name: "implements fmt.Stringer", value: &mockStringer{value: "test string"}, @@ -64,13 +161,7 @@ func TestText_Render(t *testing.T) { want: "test string", }, { - name: "does not implement fmt.Stringer or io.WriterTo", - value: struct{}{}, - wantErr: "render: cannot render", - wantErrIs: []error{render.Err, render.ErrCannotRender}, - }, - { - name: "error writing to writer with io.WriterTo", + name: "io.WriterTo error", value: &mockWriterTo{ value: "test string", err: errors.New("WriteTo error!!1"), @@ -78,6 +169,31 @@ func TestText_Render(t *testing.T) { wantErr: "render: failed: WriteTo error!!1", wantErrIs: []error{render.Err, render.ErrFailed}, }, + { + name: "implements io.Reader", + value: &mockReader{value: "reader string"}, + want: "reader string", + }, + { + name: "io.Reader error", + value: &mockReader{ + value: "reader string", + err: errors.New("Read error!!1"), + }, + wantErr: "render: failed: Read error!!1", + wantErrIs: []error{render.Err, render.ErrFailed}, + }, + { + name: "error", + value: errors.New("this is an error"), + want: "this is an error", + }, + { + name: "does not implement any supported type/interface", + value: struct{}{}, + wantErr: "render: cannot render: struct {}", + wantErrIs: []error{render.Err, render.ErrCannotRender}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -101,3 +217,7 @@ func TestText_Render(t *testing.T) { }) } } + +func ptr[T any](v T) *T { + return &v +}