From b9afe213ba686e2355b7d5421b61492d03a7b349 Mon Sep 17 00:00:00 2001 From: Jed Borovik Date: Tue, 8 Sep 2015 17:28:31 -0400 Subject: [PATCH 1/6] Add null.Time and testing --- time.go | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++++ time_test.go | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 time.go create mode 100644 time_test.go diff --git a/time.go b/time.go new file mode 100644 index 0000000..8ca8d32 --- /dev/null +++ b/time.go @@ -0,0 +1,96 @@ +package null + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "reflect" + "time" +) + +// Time is a nullable time.Time. It supports SQL and JSON serialization. +// It will marshal to null if null. +type Time struct { + Time time.Time + Valid bool +} + +// Scan implements the Scanner interface. +func (t *Time) Scan(value interface{}) error { + t.Time, t.Valid = value.(time.Time) + return nil +} + +// Value implements the driver Valuer interface. +func (t Time) Value() (driver.Value, error) { + if !t.Valid { + return nil, nil + } + return t.Time, nil +} + +// NewTime creates a new Time. +func NewTime(t time.Time, valid bool) Time { + return Time{ + Time: t, + Valid: valid, + } +} + +// TimeFrom creates a new Time that will always be valid. +func TimeFrom(t time.Time) Time { + return NewTime(t, true) +} + +// TimeFromPtr creates a new Time that will be null if t is nil. +func TimeFromPtr(t *time.Time) Time { + if t == nil { + var ti time.Time + return NewTime(ti, false) + } + return NewTime(*t, true) +} + +// MarshalJSON implements json.Marshaler. +// It will encode null if this time is null. +func (t Time) MarshalJSON() ([]byte, error) { + if !t.Valid { + return []byte("null"), nil + } + return json.Marshal(t.Time) +} + +// UnmarshalJSON implements json.Unmarshaler. +// It supports time, string and null input. +func (t *Time) UnmarshalJSON(data []byte) error { + var err error + var v interface{} + json.Unmarshal(data, &v) + switch x := v.(type) { + case time.Time: + t.Time = x + case string: + t.Time, err = time.Parse(time.RFC3339, x) + case nil: + t.Valid = false + return nil + default: + err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Time", reflect.TypeOf(v).Name()) + } + t.Valid = err == nil + return err +} + +// SetValid changes this Time's value and sets it to be non-null. +func (t *Time) SetValid(v time.Time) { + t.Time = v + t.Valid = true +} + +// Ptr returns a pointer to this Time's value, or a nil pointer if this Time is null. +func (t Time) Ptr() *time.Time { + if !t.Valid { + return nil + } + return &t.Time +} diff --git a/time_test.go b/time_test.go new file mode 100644 index 0000000..390dc08 --- /dev/null +++ b/time_test.go @@ -0,0 +1,96 @@ +package null + +import ( + "encoding/json" + "testing" + "time" +) + +var ( + timeString = "2012-12-21T21:21:21Z" + timeJSON = []byte(`"` + timeString + `"`) + blankTimeJSON = []byte(`null`) + timeValue, _ = time.Parse(time.RFC3339, timeString) +) + +func TestUnmarshalTimeString(t *testing.T) { + var ti Time + err := json.Unmarshal(timeJSON, &ti) + maybePanic(err) + assertTime(t, ti, "UnmarshalJSON() json") + + var blank Time + err = json.Unmarshal(blankTimeJSON, &blank) + maybePanic(err) + assertNullTime(t, blank, "blank time json") +} + +func TestMarshalTime(t *testing.T) { + ti := TimeFrom(timeValue) + data, err := json.Marshal(ti) + maybePanic(err) + assertJSONEquals(t, data, string(timeJSON), "non-empty json marshal") +} + +func TestTimeFrom(t *testing.T) { + ti := TimeFrom(timeValue) + assertTime(t, ti, "TimeFrom() time.Time") +} + +func TestTimeFromPtr(t *testing.T) { + ti := TimeFromPtr(&timeValue) + assertTime(t, ti, "TimeFromPtr() time") + + null := TimeFromPtr(nil) + assertNullTime(t, null, "TimeFromPtr(nil)") +} + +func TestTimeSetValid(t *testing.T) { + var ti time.Time + change := NewTime(ti, false) + assertNullTime(t, change, "SetValid()") + change.SetValid(timeValue) + assertTime(t, change, "SetValid()") +} + +func TestTimePointer(t *testing.T) { + ti := TimeFrom(timeValue) + ptr := ti.Ptr() + if *ptr != timeValue { + t.Errorf("bad %s time: %#v ≠ %v\n", "pointer", ptr, timeValue) + } + + var nt time.Time + null := NewTime(nt, false) + ptr = null.Ptr() + if ptr != nil { + t.Errorf("bad %s time: %#v ≠ %s\n", "nil pointer", ptr, "nil") + } +} + +func TestTimeScan(t *testing.T) { + var ti Time + err := ti.Scan(timeValue) + maybePanic(err) + assertTime(t, ti, "scanned time") + + var null Time + err = null.Scan(nil) + maybePanic(err) + assertNullTime(t, null, "scanned null") +} + +func assertTime(t *testing.T, ti Time, from string) { + if ti.Time != timeValue { + t.Errorf("bad %v time: %v ≠ %v\n", from, ti.Time, timeValue) + } + if !ti.Valid { + t.Error(from, "is invalid, but should be valid") + } +} + +func assertNullTime(t *testing.T, ti Time, from string) { + if ti.Valid { + t.Error(from, "is valid, but should be invalid") + } +} From 8112aae462880531948172764d0f8f77d2a62dec Mon Sep 17 00:00:00 2001 From: Jed Borovik Date: Thu, 10 Sep 2015 10:56:15 -0400 Subject: [PATCH 2/6] Make null.Time.Scan more robust by returning an error on the wrong type --- time.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/time.go b/time.go index 8ca8d32..33a49cf 100644 --- a/time.go +++ b/time.go @@ -17,8 +17,15 @@ type Time struct { // Scan implements the Scanner interface. func (t *Time) Scan(value interface{}) error { - t.Time, t.Valid = value.(time.Time) - return nil + var err error + switch x := value.(type) { + case time.Time: + t.Time = x + default: + err = fmt.Errorf("null: cannot scan type %T into null.Time: %v", value, value) + } + t.Valid = err == nil + return err } // Value implements the driver Valuer interface. From 5bebd577e02b8b2d9f77e74ea284e2c148197228 Mon Sep 17 00:00:00 2001 From: Jed Borovik Date: Thu, 10 Sep 2015 12:49:41 -0400 Subject: [PATCH 3/6] Scanning nil for null.Time creates value with Valid = false rather than erroring --- time.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/time.go b/time.go index 33a49cf..741e2a4 100644 --- a/time.go +++ b/time.go @@ -21,6 +21,9 @@ func (t *Time) Scan(value interface{}) error { switch x := value.(type) { case time.Time: t.Time = x + case nil: + t.Valid = false + return nil default: err = fmt.Errorf("null: cannot scan type %T into null.Time: %v", value, value) } From e5f4f566ed12f1ba5887bf9448b44f764899a42e Mon Sep 17 00:00:00 2001 From: Jed Borovik Date: Thu, 10 Sep 2015 13:36:31 -0400 Subject: [PATCH 4/6] Add unmarshalling null.Time from map[string]interface{} --- time.go | 23 ++++++++++++++++++----- time_test.go | 12 ++++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/time.go b/time.go index 741e2a4..d69f28c 100644 --- a/time.go +++ b/time.go @@ -71,16 +71,29 @@ func (t Time) MarshalJSON() ([]byte, error) { } // UnmarshalJSON implements json.Unmarshaler. -// It supports time, string and null input. +// It supports string, map[string]interface{}, +// and null input. func (t *Time) UnmarshalJSON(data []byte) error { var err error var v interface{} - json.Unmarshal(data, &v) + if err = json.Unmarshal(data, &v); err != nil { + return err + } switch x := v.(type) { - case time.Time: - t.Time = x case string: - t.Time, err = time.Parse(time.RFC3339, x) + err = t.Time.UnmarshalJSON(data) + case map[string]interface{}: + ti, tiOK := x["Time"].(string) + valid, validOK := x["Valid"].(bool) + if !tiOK || !validOK { + err = fmt.Errorf("json: unmarshalling map[string]interface{} into Go value of type null.Time requires key \"Time\" to be of type string and key \"Valid\" to be of type bool; found %T and %T, respectively", x["Time"], x["Valid"]) + break + } + err = t.Time.UnmarshalJSON([]byte(`"` + ti + `"`)) + t.Valid = valid + if err == nil && t.Valid == false { + return nil // Return here to prevent `Valid` from being set to true based on nil error below. + } case nil: t.Valid = false return nil diff --git a/time_test.go b/time_test.go index 390dc08..aebfec1 100644 --- a/time_test.go +++ b/time_test.go @@ -11,6 +11,8 @@ var ( timeJSON = []byte(`"` + timeString + `"`) blankTimeJSON = []byte(`null`) timeValue, _ = time.Parse(time.RFC3339, timeString) + timeMap = []byte(`{"Time":"2012-12-21T21:21:21Z","Valid":true}`) + invalidMap = []byte(`{"Time":"0001-01-01T00:00:00Z","Valid":false}`) ) func TestUnmarshalTimeString(t *testing.T) { @@ -23,6 +25,16 @@ func TestUnmarshalTimeString(t *testing.T) { err = json.Unmarshal(blankTimeJSON, &blank) maybePanic(err) assertNullTime(t, blank, "blank time json") + + var fromMap Time + err = json.Unmarshal(timeMap, &fromMap) + maybePanic(err) + assertTime(t, fromMap, "map time json") + + var invalid Time + err = json.Unmarshal(invalidMap, &invalid) + maybePanic(err) + assertNullTime(t, invalid, "map invalid time json") } func TestMarshalTime(t *testing.T) { From 639d4dad0a3c8071932666e67e3dc86b02ee9e38 Mon Sep 17 00:00:00 2001 From: Jed Borovik Date: Sat, 12 Sep 2015 18:48:38 -0400 Subject: [PATCH 5/6] Minor tweaks to null.Time UnmarshalJSON --- time.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/time.go b/time.go index d69f28c..474a2cf 100644 --- a/time.go +++ b/time.go @@ -86,14 +86,11 @@ func (t *Time) UnmarshalJSON(data []byte) error { ti, tiOK := x["Time"].(string) valid, validOK := x["Valid"].(bool) if !tiOK || !validOK { - err = fmt.Errorf("json: unmarshalling map[string]interface{} into Go value of type null.Time requires key \"Time\" to be of type string and key \"Valid\" to be of type bool; found %T and %T, respectively", x["Time"], x["Valid"]) - break + return fmt.Errorf("json: unmarshalling JSON object into Go value of type null.Time requires key \"Time\" to be of type string and key \"Valid\" to be of type bool; found %T and %T, respectively", x["Time"], x["Valid"]) } err = t.Time.UnmarshalJSON([]byte(`"` + ti + `"`)) t.Valid = valid - if err == nil && t.Valid == false { - return nil // Return here to prevent `Valid` from being set to true based on nil error below. - } + return err case nil: t.Valid = false return nil From 50fbbe2bd86fe49d12adbbea0c0188bfae822033 Mon Sep 17 00:00:00 2001 From: Jed Borovik Date: Sat, 12 Sep 2015 20:30:39 -0400 Subject: [PATCH 6/6] Add zero.Time and tests --- time.go | 2 +- zero/time.go | 126 +++++++++++++++++++++++++++++++++++++++++ zero/time_test.go | 140 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 zero/time.go create mode 100644 zero/time_test.go diff --git a/time.go b/time.go index 474a2cf..afdfe9a 100644 --- a/time.go +++ b/time.go @@ -86,7 +86,7 @@ func (t *Time) UnmarshalJSON(data []byte) error { ti, tiOK := x["Time"].(string) valid, validOK := x["Valid"].(bool) if !tiOK || !validOK { - return fmt.Errorf("json: unmarshalling JSON object into Go value of type null.Time requires key \"Time\" to be of type string and key \"Valid\" to be of type bool; found %T and %T, respectively", x["Time"], x["Valid"]) + return fmt.Errorf("json: unmarshalling object into Go value of type null.Time requires key \"Time\" to be of type string and key \"Valid\" to be of type bool; found %T and %T, respectively", x["Time"], x["Valid"]) } err = t.Time.UnmarshalJSON([]byte(`"` + ti + `"`)) t.Valid = valid diff --git a/zero/time.go b/zero/time.go new file mode 100644 index 0000000..f2b3852 --- /dev/null +++ b/zero/time.go @@ -0,0 +1,126 @@ +package zero + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "reflect" + "time" +) + +// Time is a nullable time.Time. +// JSON marshals to the zero value for time.Time if null. +// Considered to be null to SQL if zero. +type Time struct { + Time time.Time + Valid bool +} + +// Scan implements Scanner interface. +func (t *Time) Scan(value interface{}) error { + var err error + switch x := value.(type) { + case time.Time: + t.Time = x + case nil: + t.Valid = false + return nil + default: + err = fmt.Errorf("null: cannot scan type %T into null.Time: %v", value, value) + } + t.Valid = err == nil + return err +} + +// Value implements the driver Valuer interface. +func (t Time) Value() (driver.Value, error) { + if !t.Valid { + return nil, nil + } + return t.Time, nil +} + +// NewTime creates a new Time. +func NewTime(t time.Time, valid bool) Time { + return Time{ + Time: t, + Valid: valid, + } +} + +// TimeFrom creates a new Time that will +// be null if t is the zero value. +func TimeFrom(t time.Time) Time { + return NewTime(t, !t.IsZero()) +} + +// TimeFromPtr creates a new Time that will +// be null if t is nil or *t is the zero value. +func TimeFromPtr(t *time.Time) Time { + if t == nil { + var ti time.Time + return NewTime(ti, false) + } + return TimeFrom(*t) +} + +// MarshalJSON implements json.Marshaler. +// It will encode the zero value of time.Time +// if this time is invalid. +func (t Time) MarshalJSON() ([]byte, error) { + if !t.Valid { + var ti time.Time + return json.Marshal(ti) + } + return json.Marshal(t.Time) +} + +// UnmarshalJSON implements json.Unmarshaler. +// It supports string, map[string]interface{}, +// and null input. +func (t *Time) UnmarshalJSON(data []byte) error { + var err error + var v interface{} + if err = json.Unmarshal(data, &v); err != nil { + return err + } + switch x := v.(type) { + case string: + var ti time.Time + if err = ti.UnmarshalJSON(data); err != nil { + return err + } + *t = TimeFrom(ti) + return nil + case map[string]interface{}: + ti, tiOK := x["Time"].(string) + valid, validOK := x["Valid"].(bool) + if !tiOK || !validOK { + return fmt.Errorf("json: unmarshalling object into Go value of type null.Time requires key \"Time\" to be of type string and key \"Valid\" to be of type bool; found %T and %T, respectively", x["Time"], x["Valid"]) + } + err = t.Time.UnmarshalJSON([]byte(`"` + ti + `"`)) + t.Valid = valid + return err + case nil: + t.Valid = false + return nil + default: + return fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Time", reflect.TypeOf(v).Name()) + } +} + +// SetValid changes this Time's value and +// sets it to be non-null. +func (t *Time) SetValid(v time.Time) { + t.Time = v + t.Valid = true +} + +// Ptr returns a pointer to this Time's value, +// or a nil pointer if this Time is zero. +func (t Time) Ptr() *time.Time { + if !t.Valid { + return nil + } + return &t.Time +} diff --git a/zero/time_test.go b/zero/time_test.go new file mode 100644 index 0000000..bb6db03 --- /dev/null +++ b/zero/time_test.go @@ -0,0 +1,140 @@ +package zero + +import ( + "encoding/json" + "testing" + "time" +) + +var ( + timeString = "2012-12-21T21:21:21Z" + timeJSON = []byte(`"` + timeString + `"`) + zeroTimeJSON = []byte(`"0001-01-01T00:00:00Z"`) + blankTimeJSON = []byte(`null`) + timeValue, _ = time.Parse(time.RFC3339, timeString) + timeMap = []byte(`{"Time":"2012-12-21T21:21:21Z","Valid":true}`) + invalidMap = []byte(`{"Time":"0001-01-01T00:00:00Z","Valid":false}`) +) + +func TestUnmarshalTimeString(t *testing.T) { + var ti Time + err := json.Unmarshal(timeMap, &ti) + maybePanic(err) + assertTime(t, ti, "UnmarshalJSON() json") + + var blank Time + err = json.Unmarshal(blankTimeJSON, &blank) + maybePanic(err) + assertNullTime(t, blank, "blank time json") + + var zero Time + err = json.Unmarshal(zeroTimeJSON, &zero) + maybePanic(err) + assertNullTime(t, zero, "zero time json") + + var fromMap Time + err = json.Unmarshal(timeMap, &fromMap) + maybePanic(err) + assertTime(t, fromMap, "map time json") + + var invalid Time + err = json.Unmarshal(invalidMap, &invalid) + maybePanic(err) + assertNullTime(t, invalid, "map invalid time json") +} + +func TestMarshalTime(t *testing.T) { + ti := TimeFrom(timeValue) + data, err := json.Marshal(ti) + maybePanic(err) + assertJSONEquals(t, data, string(timeJSON), "non-empty json marshal") + + null := TimeFromPtr(nil) + data, err = json.Marshal(null) + maybePanic(err) + assertJSONEquals(t, data, string(zeroTimeJSON), "empty json marshal") +} + +func TestTimeFrom(t *testing.T) { + ti := TimeFrom(timeValue) + assertTime(t, ti, "TimeFrom() time.Time") + + var nt time.Time + null := TimeFrom(nt) + assertNullTime(t, null, "TimeFrom() empty time.Time") +} + +func TestTimeFromPtr(t *testing.T) { + ti := TimeFromPtr(&timeValue) + assertTime(t, ti, "TimeFromPtr() time") + + null := TimeFromPtr(nil) + assertNullTime(t, null, "TimeFromPtr(nil)") +} + +func TestTimeSetValid(t *testing.T) { + var ti time.Time + change := TimeFrom(ti) + assertNullTime(t, change, "SetValid()") + change.SetValid(timeValue) + assertTime(t, change, "SetValid()") +} + +func TestTimePointer(t *testing.T) { + ti := TimeFrom(timeValue) + ptr := ti.Ptr() + if *ptr != timeValue { + t.Errorf("bad %s time: %#v ≠ %v\n", "pointer", ptr, timeValue) + } + + var nt time.Time + null := TimeFrom(nt) + ptr = null.Ptr() + if ptr != nil { + t.Errorf("bad %s time: %#v ≠ %s\n", "nil pointer", ptr, "nil") + } +} + +func TestTimeScan(t *testing.T) { + var ti Time + err := ti.Scan(timeValue) + maybePanic(err) + assertTime(t, ti, "scanned time") + + var null Time + err = null.Scan(nil) + maybePanic(err) + assertNullTime(t, null, "scanned null") +} + +func TestTimeValue(t *testing.T) { + ti := TimeFrom(timeValue) + v, err := ti.Value() + maybePanic(err) + if ti.Time != timeValue { + t.Errorf("bad time.Time value: %v ≠ %v", ti.Time, timeValue) + } + + var nt time.Time + zero := TimeFrom(nt) + v, err = zero.Value() + maybePanic(err) + if v != nil { + t.Errorf("bad %s time.Time value: %v ≠ %v", "zero", v, nil) + } +} + +func assertTime(t *testing.T, ti Time, from string) { + if ti.Time != timeValue { + t.Errorf("bad %v time: %v ≠ %v\n", from, ti.Time, timeValue) + } + if !ti.Valid { + t.Error(from, "is invalid, but should be valid") + } +} + +func assertNullTime(t *testing.T, ti Time, from string) { + if ti.Valid { + t.Error(from, "is valid, but should be invalid") + } +}