commit
e6fc057718
4 changed files with 490 additions and 0 deletions
116
time.go
Normal file
116
time.go
Normal file
|
@ -0,0 +1,116 @@
|
|||
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 {
|
||||
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 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 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:
|
||||
err = t.Time.UnmarshalJSON(data)
|
||||
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:
|
||||
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
|
||||
}
|
108
time_test.go
Normal file
108
time_test.go
Normal file
|
@ -0,0 +1,108 @@
|
|||
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)
|
||||
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(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")
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
126
zero/time.go
Normal file
126
zero/time.go
Normal file
|
@ -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
|
||||
}
|
140
zero/time_test.go
Normal file
140
zero/time_test.go
Normal file
|
@ -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")
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue