Skip to content

Commit 0ef4469

Browse files
committed
encoder,decoder: Using timeformatter when provided or default
1 parent 136a6e5 commit 0ef4469

4 files changed

Lines changed: 146 additions & 4 deletions

File tree

decode.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ type Decoder interface {
2626
// StdDecoder is the default implementation of the Decoder interface.
2727
type StdDecoder struct {
2828
skipUnknownFields bool
29+
30+
// timeFormatter is optional - defaultTimeFormatter is used when unset.
31+
timeFormatter TimeFormatter
2932
}
3033

3134
func (d *StdDecoder) DecodeRaw(body []byte, v interface{}) error {
@@ -271,7 +274,8 @@ func (d *StdDecoder) decodeDateTime(value string) (time.Time, error) {
271274
if value == "" {
272275
return time.Time{}, nil
273276
}
274-
return time.Parse(time.RFC3339, value)
277+
278+
return timeFormatterOrDefault(d.timeFormatter).ParseTime(value)
275279
}
276280

277281
func findFieldByNameOrTag(field reflect.Value, fName string) reflect.Value {

decode_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,3 +667,56 @@ func loadTestFile(t *testing.T, name string) []byte {
667667

668668
return bytes
669669
}
670+
671+
func Test_decodeDateTime_withTimeFormatter(t *testing.T) {
672+
tests := []struct {
673+
name string
674+
decoder *StdDecoder
675+
input string
676+
expect string
677+
expectErr bool
678+
}{
679+
{
680+
name: "unset formatter retains RFC3339",
681+
decoder: &StdDecoder{},
682+
input: "2019-10-11T13:40:30Z",
683+
expect: "2019-10-11T13:40:30Z",
684+
},
685+
{
686+
name: "unset formatter rejects compact form",
687+
decoder: &StdDecoder{},
688+
input: "20191011T13:40:30",
689+
expectErr: true,
690+
},
691+
{
692+
name: "custom formatter",
693+
decoder: &StdDecoder{
694+
timeFormatter: &LayoutTimeFormatter{FormatLayout: LayoutISO8601Compact},
695+
},
696+
input: "20191011T13:40:30",
697+
expect: "2019-10-11T13:40:30Z",
698+
},
699+
{
700+
name: "empty value decodes to zero time regardless of formatter",
701+
decoder: &StdDecoder{
702+
timeFormatter: &LayoutTimeFormatter{FormatLayout: LayoutISO8601Compact},
703+
},
704+
input: "",
705+
expect: "0001-01-01T00:00:00Z",
706+
},
707+
}
708+
709+
for _, tt := range tests {
710+
t.Run(tt.name, func(t *testing.T) {
711+
got, err := tt.decoder.decodeDateTime(tt.input)
712+
if tt.expectErr {
713+
require.Error(t, err)
714+
715+
return
716+
}
717+
718+
require.NoError(t, err)
719+
require.Equal(t, tt.expect, got.Format(time.RFC3339))
720+
})
721+
}
722+
}

encode.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ type Encoder interface {
1515
}
1616

1717
// StdEncoder is the default implementation of Encoder interface.
18-
type StdEncoder struct{}
18+
type StdEncoder struct {
19+
// timeFormatter is optional - defaultTimeFormatter is used when unset.
20+
timeFormatter TimeFormatter
21+
}
1922

2023
func (e *StdEncoder) Encode(w io.Writer, methodName string, args interface{}) error {
2124
_, _ = fmt.Fprintf(w, "<methodCall><methodName>%s</methodName>", methodName)
@@ -251,8 +254,21 @@ func (e *StdEncoder) encodeBase64(w io.Writer, val []byte) error {
251254
}
252255

253256
func (e *StdEncoder) encodeTime(w io.Writer, val time.Time) error {
254-
_, err := fmt.Fprintf(w, "<dateTime.iso8601>%s</dateTime.iso8601>", val.Format(time.RFC3339))
255-
return err
257+
formatted := timeFormatterOrDefault(e.timeFormatter).FormatTime(val)
258+
259+
if _, err := fmt.Fprint(w, "<dateTime.iso8601>"); err != nil {
260+
return err
261+
}
262+
263+
if err := xml.EscapeText(w, []byte(formatted)); err != nil {
264+
return fmt.Errorf("failed to escape time value: %w", err)
265+
}
266+
267+
if _, err := fmt.Fprint(w, "</dateTime.iso8601>"); err != nil {
268+
return err
269+
}
270+
271+
return nil
256272
}
257273

258274
func (e *StdEncoder) encodeMap(w io.Writer, val interface{}) error {

encode_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package xmlrpc
22

33
import (
4+
"errors"
45
"fmt"
56
"strings"
67
"testing"
@@ -579,3 +580,71 @@ func Test_encodeMap(t *testing.T) {
579580
})
580581
}
581582
}
583+
584+
func Test_encodeTime_withTimeFormatter(t *testing.T) {
585+
input := time.Date(2019, 10, 11, 13, 40, 30, 0, time.UTC)
586+
587+
tests := []struct {
588+
name string
589+
encoder *StdEncoder
590+
expect string
591+
}{
592+
{
593+
name: "unset formatter retains RFC3339",
594+
encoder: &StdEncoder{},
595+
expect: "<dateTime.iso8601>2019-10-11T13:40:30Z</dateTime.iso8601>",
596+
},
597+
{
598+
name: "custom formatter",
599+
encoder: &StdEncoder{
600+
timeFormatter: &LayoutTimeFormatter{FormatLayout: LayoutISO8601Compact},
601+
},
602+
expect: "<dateTime.iso8601>20191011T13:40:30</dateTime.iso8601>",
603+
},
604+
}
605+
606+
for _, tt := range tests {
607+
t.Run(tt.name, func(t *testing.T) {
608+
buf := new(strings.Builder)
609+
require.NoError(t, tt.encoder.encodeTime(buf, input))
610+
require.Equal(t, tt.expect, buf.String())
611+
})
612+
}
613+
}
614+
615+
// failingWriter fails once more than limit bytes have been written, so a failure can be
616+
// injected at any point of an element.
617+
type failingWriter struct {
618+
limit int
619+
written int
620+
}
621+
622+
func (w *failingWriter) Write(p []byte) (int, error) {
623+
if w.written+len(p) > w.limit {
624+
return 0, errors.New("write failed")
625+
}
626+
w.written += len(p)
627+
628+
return len(p), nil
629+
}
630+
631+
func Test_encodeTime_writerErrors(t *testing.T) {
632+
input := time.Date(2019, 10, 11, 13, 40, 30, 0, time.UTC)
633+
634+
// Full output is "<dateTime.iso8601>2019-10-11T13:40:30Z</dateTime.iso8601>"
635+
tests := []struct {
636+
name string
637+
limit int
638+
}{
639+
{name: "fails on opening tag", limit: 0},
640+
{name: "fails on value", limit: len("<dateTime.iso8601>")},
641+
{name: "fails on closing tag", limit: len("<dateTime.iso8601>2019-10-11T13:40:30Z")},
642+
}
643+
644+
for _, tt := range tests {
645+
t.Run(tt.name, func(t *testing.T) {
646+
err := (&StdEncoder{}).encodeTime(&failingWriter{limit: tt.limit}, input)
647+
require.Error(t, err, "writer failure must not be discarded")
648+
})
649+
}
650+
}

0 commit comments

Comments
 (0)