From f81515179829583c4535a52adda984ae07980507 Mon Sep 17 00:00:00 2001 From: avalchev94 Date: Tue, 18 Sep 2018 15:50:54 +0300 Subject: [PATCH] Added implementation for the following interfaces: encoding.BinaryMarshaler, sql.Scanner and driver.Valuer. The design of the implementations follow closely the Date's implementation. Furthermore, added tests for the new methods. --- clock/marshal.go | 43 ++++++++++++ clock/marshal_test.go | 149 ++++++++++++++++++++++++++++++++++++++++++ clock/sql.go | 41 ++++++++++++ clock/sql_test.go | 73 +++++++++++++++++++++ 4 files changed, 306 insertions(+) create mode 100644 clock/marshal.go create mode 100644 clock/marshal_test.go create mode 100644 clock/sql.go create mode 100644 clock/sql_test.go diff --git a/clock/marshal.go b/clock/marshal.go new file mode 100644 index 00000000..6121b71a --- /dev/null +++ b/clock/marshal.go @@ -0,0 +1,43 @@ +package clock + +import ( + "errors" +) + +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (c Clock) MarshalBinary() ([]byte, error) { + enc := []byte{ + byte(c >> 24), + byte(c >> 16), + byte(c >> 8), + byte(c), + } + return enc, nil +} + +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (c *Clock) UnmarshalBinary(data []byte) error { + if len(data) == 0 { + return errors.New("Clock.UnmarshalBinary: no data") + } + if len(data) != 4 { + return errors.New("Clock.UnmarshalBinary: invalid length") + } + + *c = Clock(data[3]) | Clock(data[2])<<8 | Clock(data[1])<<16 | Clock(data[0])<<24 + return nil +} + +// MarshalText implements the encoding.TextMarshaler interface. +func (c Clock) MarshalText() ([]byte, error) { + return []byte(c.String()), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface. +func (c *Clock) UnmarshalText(data []byte) (err error) { + clock, err := Parse(string(data)) + if err == nil { + *c = clock + } + return err +} diff --git a/clock/marshal_test.go b/clock/marshal_test.go new file mode 100644 index 00000000..1a9fa655 --- /dev/null +++ b/clock/marshal_test.go @@ -0,0 +1,149 @@ +package clock + +import ( + "bytes" + "encoding/gob" + "encoding/json" + "strings" + "testing" +) + +func TestGobEncoding(t *testing.T) { + var b bytes.Buffer + encoder := gob.NewEncoder(&b) + decoder := gob.NewDecoder(&b) + cases := []Clock{ + New(-1, -1, -1, -1), + New(0, 0, 0, 0), + New(12, 40, 40, 80), + New(13, 55, 0, 20), + New(16, 20, 0, 0), + New(20, 60, 59, 59), + New(24, 0, 0, 0), + New(24, 0, 0, 1), + } + for _, c := range cases { + var clock Clock + err := encoder.Encode(&c) + if err != nil { + t.Errorf("Gob(%v) encode error %v", c, err) + } else { + err = decoder.Decode(&clock) + if err != nil { + t.Errorf("Gob(%v) decode error %v", c, err) + } else if clock != c { + t.Errorf("Gob(%v) decode got %v", c, clock) + } + } + } +} + +func TestJSONMarshalling(t *testing.T) { + cases := []struct { + value Clock + want string + }{ + {New(-1, -1, -1, -1), `"22:58:58.999"`}, + {New(0, 0, 0, 0), `"00:00:00.000"`}, + {New(12, 40, 40, 80), `"12:40:40.080"`}, + {New(13, 55, 0, 20), `"13:55:00.020"`}, + {New(16, 20, 0, 0), `"16:20:00.000"`}, + {New(20, 60, 59, 59), `"21:00:59.059"`}, + {New(24, 0, 0, 0), `"24:00:00.000"`}, + {New(24, 0, 0, 1), `"00:00:00.001"`}, + } + for _, c := range cases { + bb, err := json.Marshal(c.value) + if err != nil { + t.Errorf("JSON(%v) marshal error %v", c, err) + } else if string(bb) != c.want { + t.Errorf("JSON(%v) == %v, want %v", c.value, string(bb), c.want) + } + } +} + +func TestJSONUnmarshalling(t *testing.T) { + cases := []struct { + values []string + want Clock + }{ + {[]string{`"22:58:58.999"`, `"10:58:58.999pm"`}, New(-1, -1, -1, -1)}, + {[]string{`"00:00:00.000"`, `"00:00:00.000AM"`}, New(0, 0, 0, 0)}, + {[]string{`"12:40:40.080"`, `"12:40:40.080PM"`}, New(12, 40, 40, 80)}, + {[]string{`"13:55:00.020"`, `"01:55:00.020PM"`}, New(13, 55, 0, 20)}, + {[]string{`"16:20:00.000"`, `"04:20:00.000pm"`}, New(16, 20, 0, 0)}, + {[]string{`"21:00:59.059"`, `"09:00:59.059PM"`}, New(20, 60, 59, 59)}, + {[]string{`"24:00:00.000"`, `"00:00:00.000am"`}, New(24, 0, 0, 0)}, + {[]string{`"00:00:00.001"`, `"00:00:00.001AM"`}, New(24, 0, 0, 1)}, + } + + for _, c := range cases { + for _, v := range c.values { + var clock Clock + err := json.Unmarshal([]byte(v), &clock) + if err != nil { + t.Errorf("JSON(%v) unmarshal error %v", v, err) + } else if c.want.Mod24() != clock.Mod24() { + t.Errorf("JSON(%v) == %v, want %v", v, clock, c.want) + } + } + } +} + +func TestBinaryMarshalling(t *testing.T) { + cases := []Clock{ + New(-1, -1, -1, -1), + New(0, 0, 0, 0), + New(12, 40, 40, 80), + New(13, 55, 0, 20), + New(16, 20, 0, 0), + New(20, 60, 59, 59), + New(24, 0, 0, 0), + New(24, 0, 0, 1), + } + for _, c := range cases { + bb, err := c.MarshalBinary() + if err != nil { + t.Errorf("Binary(%v) marshal error %v", c, err) + } else { + var clock Clock + err = clock.UnmarshalBinary(bb) + if err != nil { + t.Errorf("Binary(% v) unmarshal error %v", c, err) + } else if clock.Mod24() != c.Mod24() { + t.Errorf("Binary(%v) unmarshal got %v", c, clock) + } + } + } +} + +func TestBinaryUnmarshallingErrors(t *testing.T) { + var c Clock + err1 := c.UnmarshalBinary([]byte{}) + if err1 == nil { + t.Errorf("unmarshal no empty data error") + } + + err2 := c.UnmarshalBinary([]byte("12345")) + if err2 == nil { + t.Errorf("unmarshal no wrong length error") + } +} + +func TestInvalidClockText(t *testing.T) { + cases := []struct { + value string + want string + }{ + {`not-a-clock`, `clock.Clock: cannot parse not-a-clock`}, + {`00:50:100.0`, `clock.Clock: cannot parse 00:50:100.0`}, + {`24:00:00.0pM`, `clock.Clock: cannot parse 24:00:00.0pM: strconv.Atoi: parsing "0pM": invalid syntax`}, + } + for _, c := range cases { + var clock Clock + err := clock.UnmarshalText([]byte(c.value)) + if err == nil || !strings.Contains(err.Error(), c.want) { + t.Errorf("InvalidText(%v) == %v, want %v", c.value, err, c.want) + } + } +} diff --git a/clock/sql.go b/clock/sql.go new file mode 100644 index 00000000..87903c2e --- /dev/null +++ b/clock/sql.go @@ -0,0 +1,41 @@ +package clock + +import ( + "database/sql/driver" + "fmt" + "time" +) + +// Scan parses some value. It implements sql.Scanner, +// https://golang.org/pkg/database/sql/#Scanner +func (c *Clock) Scan(value interface{}) (err error) { + if value == nil { + return nil + } + + return c.scanAny(value) +} + +func (c *Clock) scanAny(value interface{}) (err error) { + err = nil + switch value.(type) { + case int64: + *c = Clock(value.(int64)) + case []byte: + *c, err = Parse(string(value.([]byte))) + case string: + *c, err = Parse(value.(string)) + case time.Time: + *c = NewAt(value.(time.Time)) + default: + err = fmt.Errorf("%T %+v is not a meaningful clock", value, value) + } + return +} + +// Value converts the value to an int64. It implements driver.Valuer, +// https://golang.org/pkg/database/sql/driver/#Valuer +func (c Clock) Value() (driver.Value, error) { + + return int64(c), nil +} diff --git a/clock/sql_test.go b/clock/sql_test.go new file mode 100644 index 00000000..88a27812 --- /dev/null +++ b/clock/sql_test.go @@ -0,0 +1,73 @@ +package clock + +import ( + "database/sql/driver" + "testing" + "time" +) + +func TestClockScan(t *testing.T) { + now := time.Now() + + cases := []struct { + v interface{} + expected Clock + }{ + {int64(New(-1, -1, -1, -1)), New(-1, -1, -1, -1)}, + {int64(New(10, 60, 10, 0)), New(10, 60, 10, 0)}, + {int64(New(24, 10, 0, 10)), New(0, 10, 0, 10)}, + {"12:00:00.400", New(12, 0, 0, 400)}, + {"01:40:50.000pm", New(13, 40, 50, 0)}, + {"4:20:00.000pm", New(16, 20, 0, 0)}, + {[]byte("23:60:60.000"), New(0, 1, 0, 0)}, + {now, NewAt(now)}, + } + + for i, c := range cases { + var clock Clock + e := clock.Scan(c.v) + if e != nil { + t.Errorf("%d: Got %v for %d", i, e, c.expected) + } else if clock.Mod24() != c.expected.Mod24() { + t.Errorf("%d: Got %v, want %d", i, clock, c.expected) + } + + var d driver.Valuer = clock + + q, e := d.Value() + if e != nil { + t.Errorf("%d: Got %v for %d", i, e, c.expected) + } else if Clock(q.(int64)).Mod24() != c.expected.Mod24() { + t.Errorf("%d: Got %v, want %d", i, q, c.expected) + } + } +} + +func TestClockScanWithJunk(t *testing.T) { + cases := []struct { + v interface{} + expected string + }{ + {true, "bool true is not a meaningful clock"}, + {false, "bool false is not a meaningful clock"}, + } + + for i, c := range cases { + var clock Clock + e := clock.Scan(c.v) + if e.Error() != c.expected { + t.Errorf("%d: Got %q, want %q", i, e.Error(), c.expected) + } + } +} + +func TestClockScanWithNil(t *testing.T) { + var r *Clock + e := r.Scan(nil) + if e != nil { + t.Errorf("Got %v", e) + } + if r != nil { + t.Errorf("Got %v", r) + } +}