From bbb7b84bb6afb50b9c30e762f766a2a4c22395c2 Mon Sep 17 00:00:00 2001 From: Patrick O'brien Date: Tue, 17 May 2016 22:02:36 +1000 Subject: [PATCH] Add all int and float null types * Add Scan and Value methods for non sql.Int64 and sql.Float64 types * Change NullInt to use int in the code opposed to int64 * Rename Float to Float64 * Add support for all missing key int and float types: float32, int8, int16, int32, int64, uint8, uint16, uint32, uint64 * Update README to include new additions/changes --- README.md | 88 +++++++++++--- convert.go | 266 +++++++++++++++++++++++++++++++++++++++++ float32.go | 144 ++++++++++++++++++++++ float32_test.go | 169 ++++++++++++++++++++++++++ float.go => float64.go | 56 ++++----- float64_test.go | 169 ++++++++++++++++++++++++++ float_test.go | 169 -------------------------- int.go | 65 +++++++--- int16.go | 145 ++++++++++++++++++++++ int16_test.go | 196 ++++++++++++++++++++++++++++++ int32.go | 145 ++++++++++++++++++++++ int32_test.go | 196 ++++++++++++++++++++++++++++++ int64.go | 118 ++++++++++++++++++ int64_test.go | 196 ++++++++++++++++++++++++++++++ int8.go | 145 ++++++++++++++++++++++ int8_test.go | 196 ++++++++++++++++++++++++++++++ int_test.go | 30 +---- uint.go | 145 ++++++++++++++++++++++ uint16.go | 145 ++++++++++++++++++++++ uint16_test.go | 196 ++++++++++++++++++++++++++++++ uint32.go | 145 ++++++++++++++++++++++ uint32_test.go | 196 ++++++++++++++++++++++++++++++ uint64.go | 145 ++++++++++++++++++++++ uint64_test.go | 178 +++++++++++++++++++++++++++ uint8.go | 145 ++++++++++++++++++++++ uint8_test.go | 196 ++++++++++++++++++++++++++++++ uint_test.go | 178 +++++++++++++++++++++++++++ 27 files changed, 3903 insertions(+), 259 deletions(-) create mode 100644 convert.go create mode 100644 float32.go create mode 100644 float32_test.go rename float.go => float64.go (53%) create mode 100644 float64_test.go delete mode 100644 float_test.go create mode 100644 int16.go create mode 100644 int16_test.go create mode 100644 int32.go create mode 100644 int32_test.go create mode 100644 int64.go create mode 100644 int64_test.go create mode 100644 int8.go create mode 100644 int8_test.go create mode 100644 uint.go create mode 100644 uint16.go create mode 100644 uint16_test.go create mode 100644 uint32.go create mode 100644 uint32_test.go create mode 100644 uint64.go create mode 100644 uint64_test.go create mode 100644 uint8.go create mode 100644 uint8_test.go create mode 100644 uint_test.go diff --git a/README.md b/README.md index 62bf48b..98e829a 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,13 @@ null is a library with reasonable options for dealing with nullable SQL and JSON values -There are two packages: `null` and its subpackage `zero`. +There are two packages: `null` and its subpackage `zero`. Types in `null` will only be considered null on null input, and will JSON encode to `null`. If you need zero and null be considered separate values, use these. Types in `zero` are treated like zero values in Go: blank string input will produce a null `zero.String`, and null Strings will JSON encode to `""`. Zero values of these types will be considered null to SQL. If you need zero and null treated the same, use these. -All types implement `sql.Scanner` and `driver.Valuer`, so you can use this library in place of `sql.NullXXX`. All types also implement: `encoding.TextMarshaler`, `encoding.TextUnmarshaler`, `json.Marshaler`, and `json.Unmarshaler`. +All types implement `sql.Scanner` and `driver.Valuer`, so you can use this library in place of `sql.NullXXX`. All types also implement: `encoding.TextMarshaler`, `encoding.TextUnmarshaler`, `json.Marshaler`, and `json.Unmarshaler`. ### null package @@ -18,27 +18,77 @@ All types implement `sql.Scanner` and `driver.Valuer`, so you can use this libra #### null.String Nullable string. -Marshals to JSON null if SQL source data is null. Zero (blank) input will not produce a null String. Can unmarshal from `sql.NullString` JSON input or string input. - -#### null.Int -Nullable int64. - -Marshals to JSON null if SQL source data is null. Zero input will not produce a null Int. Can unmarshal from `sql.NullInt64` JSON input. - -#### null.Float -Nullable float64. - -Marshals to JSON null if SQL source data is null. Zero input will not produce a null Float. Can unmarshal from `sql.NullFloat64` JSON input. +Marshals to JSON null if SQL source data is null. Zero (blank) input will not produce a null String. Can unmarshal from `sql.NullString` JSON input or string input. #### null.Bool -Nullable bool. +Nullable bool. -Marshals to JSON null if SQL source data is null. False input will not produce a null Bool. Can unmarshal from `sql.NullBool` JSON input. +Marshals to JSON null if SQL source data is null. False input will not produce a null Bool. Can unmarshal from `sql.NullBool` JSON input. #### null.Time Marshals to JSON null if SQL source data is null. Uses `time.Time`'s marshaler. Can unmarshal from `pq.NullTime` and similar JSON input. +#### null.Float32 +Nullable float32. + +Marshals to JSON null if SQL source data is null. Zero input will not produce a null Float32. Can unmarshal from `null.NullFloat32` JSON input. + +#### null.Float64 +Nullable float64. + +Marshals to JSON null if SQL source data is null. Zero input will not produce a null Float64. Can unmarshal from `sql.NullFloat64` JSON input. + +#### null.Int +Nullable int. + +Marshals to JSON null if SQL source data is null. Zero input will not produce a null Int. Can unmarshal from `null.NullInt` JSON input. + +#### null.Int8 +Nullable int8. + +Marshals to JSON null if SQL source data is null. Zero input will not produce a null Int8. Can unmarshal from `null.NullInt8` JSON input. + +#### null.Int16 +Nullable int16. + +Marshals to JSON null if SQL source data is null. Zero input will not produce a null Int16. Can unmarshal from `null.NullInt16` JSON input. + +#### null.Int32 +Nullable int32. + +Marshals to JSON null if SQL source data is null. Zero input will not produce a null Int32. Can unmarshal from `null.NullInt32` JSON input. + +#### null.Int64 +Nullable int64. + +Marshals to JSON null if SQL source data is null. Zero input will not produce a null Int64. Can unmarshal from `sql.NullInt64` JSON input. + +#### null.Uint +Nullable uint. + +Marshals to JSON null if SQL source data is null. Zero input will not produce a null Uint. Can unmarshal from `null.NullUint` JSON input. + +#### null.Uint8 +Nullable uint8. + +Marshals to JSON null if SQL source data is null. Zero input will not produce a null Uint8. Can unmarshal from `null.NullUint8` JSON input. + +#### null.Uint16 +Nullable uint16. + +Marshals to JSON null if SQL source data is null. Zero input will not produce a null Uint16. Can unmarshal from `null.NullUint16` JSON input. + +#### null.Uint32 +Nullable int32. + +Marshals to JSON null if SQL source data is null. Zero input will not produce a null Uint32. Can unmarshal from `null.NullUint32` JSON input. + +#### null.Int64 +Nullable uint64. + +Marshals to JSON null if SQL source data is null. Zero input will not produce a null Uint64. Can unmarshal from `null.NullUint64` JSON input. + ### zero package `import "gopkg.in/guregu/null.v3/zero"` @@ -46,22 +96,22 @@ Marshals to JSON null if SQL source data is null. Uses `time.Time`'s marshaler. #### zero.String Nullable string. -Will marshal to a blank string if null. Blank string input produces a null String. Null values and zero values are considered equivalent. Can unmarshal from `sql.NullString` JSON input. +Will marshal to a blank string if null. Blank string input produces a null String. Null values and zero values are considered equivalent. Can unmarshal from `sql.NullString` JSON input. #### zero.Int Nullable int64. -Will marshal to 0 if null. 0 produces a null Int. Null values and zero values are considered equivalent. Can unmarshal from `sql.NullInt64` JSON input. +Will marshal to 0 if null. 0 produces a null Int. Null values and zero values are considered equivalent. Can unmarshal from `sql.NullInt64` JSON input. #### zero.Float Nullable float64. -Will marshal to 0 if null. 0.0 produces a null Float. Null values and zero values are considered equivalent. Can unmarshal from `sql.NullFloat64` JSON input. +Will marshal to 0 if null. 0.0 produces a null Float. Null values and zero values are considered equivalent. Can unmarshal from `sql.NullFloat64` JSON input. #### zero.Bool Nullable bool. -Will marshal to false if null. `false` produces a null Float. Null values and zero values are considered equivalent. Can unmarshal from `sql.NullBool` JSON input. +Will marshal to false if null. `false` produces a null Float. Null values and zero values are considered equivalent. Can unmarshal from `sql.NullBool` JSON input. #### zero.Time diff --git a/convert.go b/convert.go new file mode 100644 index 0000000..b66f1b3 --- /dev/null +++ b/convert.go @@ -0,0 +1,266 @@ +package null + +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Type conversions for Scan. +// These functions are copied from database/sql/convert.go build 1.6.2 + +import ( + "database/sql" + "database/sql/driver" + "errors" + "fmt" + "reflect" + "strconv" + "time" +) + +var errNilPtr = errors.New("destination pointer is nil") // embedded in descriptive error + +// convertAssign copies to dest the value in src, converting it if possible. +// An error is returned if the copy would result in loss of information. +// dest should be a pointer type. +func convertAssign(dest, src interface{}) error { + // Common cases, without reflect. + switch s := src.(type) { + case string: + switch d := dest.(type) { + case *string: + if d == nil { + return errNilPtr + } + *d = s + return nil + case *[]byte: + if d == nil { + return errNilPtr + } + *d = []byte(s) + return nil + } + case []byte: + switch d := dest.(type) { + case *string: + if d == nil { + return errNilPtr + } + *d = string(s) + return nil + case *interface{}: + if d == nil { + return errNilPtr + } + *d = cloneBytes(s) + return nil + case *[]byte: + if d == nil { + return errNilPtr + } + *d = cloneBytes(s) + return nil + case *sql.RawBytes: + if d == nil { + return errNilPtr + } + *d = s + return nil + } + case time.Time: + switch d := dest.(type) { + case *string: + *d = s.Format(time.RFC3339Nano) + return nil + case *[]byte: + if d == nil { + return errNilPtr + } + *d = []byte(s.Format(time.RFC3339Nano)) + return nil + } + case nil: + switch d := dest.(type) { + case *interface{}: + if d == nil { + return errNilPtr + } + *d = nil + return nil + case *[]byte: + if d == nil { + return errNilPtr + } + *d = nil + return nil + case *sql.RawBytes: + if d == nil { + return errNilPtr + } + *d = nil + return nil + } + } + + var sv reflect.Value + + switch d := dest.(type) { + case *string: + sv = reflect.ValueOf(src) + switch sv.Kind() { + case reflect.Bool, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64: + *d = asString(src) + return nil + } + case *[]byte: + sv = reflect.ValueOf(src) + if b, ok := asBytes(nil, sv); ok { + *d = b + return nil + } + case *sql.RawBytes: + sv = reflect.ValueOf(src) + if b, ok := asBytes([]byte(*d)[:0], sv); ok { + *d = sql.RawBytes(b) + return nil + } + case *bool: + bv, err := driver.Bool.ConvertValue(src) + if err == nil { + *d = bv.(bool) + } + return err + case *interface{}: + *d = src + return nil + } + + if scanner, ok := dest.(sql.Scanner); ok { + return scanner.Scan(src) + } + + dpv := reflect.ValueOf(dest) + if dpv.Kind() != reflect.Ptr { + return errors.New("destination not a pointer") + } + if dpv.IsNil() { + return errNilPtr + } + + if !sv.IsValid() { + sv = reflect.ValueOf(src) + } + + dv := reflect.Indirect(dpv) + if sv.IsValid() && sv.Type().AssignableTo(dv.Type()) { + dv.Set(sv) + return nil + } + + if dv.Kind() == sv.Kind() && sv.Type().ConvertibleTo(dv.Type()) { + dv.Set(sv.Convert(dv.Type())) + return nil + } + + switch dv.Kind() { + case reflect.Ptr: + if src == nil { + dv.Set(reflect.Zero(dv.Type())) + return nil + } else { + dv.Set(reflect.New(dv.Type().Elem())) + return convertAssign(dv.Interface(), src) + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + s := asString(src) + i64, err := strconv.ParseInt(s, 10, dv.Type().Bits()) + if err != nil { + err = strconvErr(err) + return fmt.Errorf("converting driver.Value type %T (%q) to a %s: %v", src, s, dv.Kind(), err) + } + dv.SetInt(i64) + return nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + s := asString(src) + u64, err := strconv.ParseUint(s, 10, dv.Type().Bits()) + if err != nil { + err = strconvErr(err) + return fmt.Errorf("converting driver.Value type %T (%q) to a %s: %v", src, s, dv.Kind(), err) + } + dv.SetUint(u64) + return nil + case reflect.Float32, reflect.Float64: + s := asString(src) + f64, err := strconv.ParseFloat(s, dv.Type().Bits()) + if err != nil { + err = strconvErr(err) + return fmt.Errorf("converting driver.Value type %T (%q) to a %s: %v", src, s, dv.Kind(), err) + } + dv.SetFloat(f64) + return nil + } + + return fmt.Errorf("unsupported Scan, storing driver.Value type %T into type %T", src, dest) +} + +func strconvErr(err error) error { + if ne, ok := err.(*strconv.NumError); ok { + return ne.Err + } + return err +} + +func cloneBytes(b []byte) []byte { + if b == nil { + return nil + } else { + c := make([]byte, len(b)) + copy(c, b) + return c + } +} + +func asString(src interface{}) string { + switch v := src.(type) { + case string: + return v + case []byte: + return string(v) + } + rv := reflect.ValueOf(src) + switch rv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return strconv.FormatInt(rv.Int(), 10) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return strconv.FormatUint(rv.Uint(), 10) + case reflect.Float64: + return strconv.FormatFloat(rv.Float(), 'g', -1, 64) + case reflect.Float32: + return strconv.FormatFloat(rv.Float(), 'g', -1, 32) + case reflect.Bool: + return strconv.FormatBool(rv.Bool()) + } + return fmt.Sprintf("%v", src) +} + +func asBytes(buf []byte, rv reflect.Value) (b []byte, ok bool) { + switch rv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return strconv.AppendInt(buf, rv.Int(), 10), true + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return strconv.AppendUint(buf, rv.Uint(), 10), true + case reflect.Float32: + return strconv.AppendFloat(buf, rv.Float(), 'g', -1, 32), true + case reflect.Float64: + return strconv.AppendFloat(buf, rv.Float(), 'g', -1, 64), true + case reflect.Bool: + return strconv.AppendBool(buf, rv.Bool()), true + case reflect.String: + s := rv.String() + return append(buf, s...), true + } + return +} diff --git a/float32.go b/float32.go new file mode 100644 index 0000000..3c6b23d --- /dev/null +++ b/float32.go @@ -0,0 +1,144 @@ +package null + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "reflect" + "strconv" +) + +// NullFloat32 is a replica of sql.NullFloat64 for float32 types. +type NullFloat32 struct { + Float32 float32 + Valid bool +} + +// Float32 is a nullable float32. +// It does not consider zero values to be null. +// It will decode to null, not zero, if null. +type Float32 struct { + NullFloat32 +} + +// NewFloat32 creates a new Float32 +func NewFloat32(f float32, valid bool) Float32 { + return Float32{ + NullFloat32: NullFloat32{ + Float32: f, + Valid: valid, + }, + } +} + +// Float32From creates a new Float32 that will always be valid. +func Float32From(f float32) Float32 { + return NewFloat32(f, true) +} + +// Float32FromPtr creates a new Float32 that be null if f is nil. +func Float32FromPtr(f *float32) Float32 { + if f == nil { + return NewFloat32(0, false) + } + return NewFloat32(*f, true) +} + +// UnmarshalJSON implements json.Unmarshaler. +// It supports number and null input. +// 0 will not be considered a null Float32. +// It also supports unmarshalling a sql.NullFloat32. +func (f *Float32) 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 float64: + f.Float32 = float32(x) + case map[string]interface{}: + err = json.Unmarshal(data, &f.NullFloat32) + case nil: + f.Valid = false + return nil + default: + err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Float32", reflect.TypeOf(v).Name()) + } + f.Valid = err == nil + return err +} + +// UnmarshalText implements encoding.TextUnmarshaler. +// It will unmarshal to a null Float32 if the input is a blank or not an integer. +// It will return an error if the input is not an integer, blank, or "null". +func (f *Float32) UnmarshalText(text []byte) error { + str := string(text) + if str == "" || str == "null" { + f.Valid = false + return nil + } + var err error + res, err := strconv.ParseFloat(string(text), 32) + f.Valid = err == nil + if f.Valid { + f.Float32 = float32(res) + } + return err +} + +// MarshalJSON implements json.Marshaler. +// It will encode null if this Float32 is null. +func (f Float32) MarshalJSON() ([]byte, error) { + if !f.Valid { + return []byte("null"), nil + } + return []byte(strconv.FormatFloat(float64(f.Float32), 'f', -1, 32)), nil +} + +// MarshalText implements encoding.TextMarshaler. +// It will encode a blank string if this Float32 is null. +func (f Float32) MarshalText() ([]byte, error) { + if !f.Valid { + return []byte{}, nil + } + return []byte(strconv.FormatFloat(float64(f.Float32), 'f', -1, 32)), nil +} + +// SetValid changes this Float32's value and also sets it to be non-null. +func (f *Float32) SetValid(n float32) { + f.Float32 = n + f.Valid = true +} + +// Ptr returns a pointer to this Float32's value, or a nil pointer if this Float32 is null. +func (f Float32) Ptr() *float32 { + if !f.Valid { + return nil + } + return &f.Float32 +} + +// IsZero returns true for invalid Float32s, for future omitempty support (Go 1.4?) +// A non-null Float32 with a 0 value will not be considered zero. +func (f Float32) IsZero() bool { + return !f.Valid +} + +// Scan implements the Scanner interface. +func (n *NullFloat32) Scan(value interface{}) error { + if value == nil { + n.Float32, n.Valid = 0, false + return nil + } + n.Valid = true + return convertAssign(&n.Float32, value) +} + +// Value implements the driver Valuer interface. +func (n NullFloat32) Value() (driver.Value, error) { + if !n.Valid { + return nil, nil + } + return n.Float32, nil +} diff --git a/float32_test.go b/float32_test.go new file mode 100644 index 0000000..278b366 --- /dev/null +++ b/float32_test.go @@ -0,0 +1,169 @@ +package null + +import ( + "encoding/json" + "testing" +) + +var ( + float32JSON = []byte(`1.2345`) + nullFloat32JSON = []byte(`{"Float32":1.2345,"Valid":true}`) +) + +func TestFloat32From(t *testing.T) { + f := Float32From(1.2345) + assertFloat32(t, f, "Float32From()") + + zero := Float32From(0) + if !zero.Valid { + t.Error("Float32From(0)", "is invalid, but should be valid") + } +} + +func TestFloat32FromPtr(t *testing.T) { + n := float32(1.2345) + iptr := &n + f := Float32FromPtr(iptr) + assertFloat32(t, f, "Float32FromPtr()") + + null := Float32FromPtr(nil) + assertNullFloat32(t, null, "Float32FromPtr(nil)") +} + +func TestUnmarshalFloat32(t *testing.T) { + var f Float32 + err := json.Unmarshal(float32JSON, &f) + maybePanic(err) + assertFloat32(t, f, "float32 json") + + var nf Float32 + err = json.Unmarshal(nullFloat32JSON, &nf) + maybePanic(err) + assertFloat32(t, nf, "sq.NullFloat32 json") + + var null Float32 + err = json.Unmarshal(nullJSON, &null) + maybePanic(err) + assertNullFloat32(t, null, "null json") + + var badType Float32 + err = json.Unmarshal(boolJSON, &badType) + if err == nil { + panic("err should not be nil") + } + assertNullFloat32(t, badType, "wrong type json") + + var invalid Float32 + err = invalid.UnmarshalJSON(invalidJSON) + if _, ok := err.(*json.SyntaxError); !ok { + t.Errorf("expected json.SyntaxError, not %T", err) + } +} + +func TestTextUnmarshalFloat32(t *testing.T) { + var f Float32 + err := f.UnmarshalText([]byte("1.2345")) + maybePanic(err) + assertFloat32(t, f, "UnmarshalText() float32") + + var blank Float32 + err = blank.UnmarshalText([]byte("")) + maybePanic(err) + assertNullFloat32(t, blank, "UnmarshalText() empty float32") + + var null Float32 + err = null.UnmarshalText([]byte("null")) + maybePanic(err) + assertNullFloat32(t, null, `UnmarshalText() "null"`) +} + +func TestMarshalFloat32(t *testing.T) { + f := Float32From(1.2345) + data, err := json.Marshal(f) + maybePanic(err) + assertJSONEquals(t, data, "1.2345", "non-empty json marshal") + + // invalid values should be encoded as null + null := NewFloat32(0, false) + data, err = json.Marshal(null) + maybePanic(err) + assertJSONEquals(t, data, "null", "null json marshal") +} + +func TestMarshalFloat32Text(t *testing.T) { + f := Float32From(1.2345) + data, err := f.MarshalText() + maybePanic(err) + assertJSONEquals(t, data, "1.2345", "non-empty text marshal") + + // invalid values should be encoded as null + null := NewFloat32(0, false) + data, err = null.MarshalText() + maybePanic(err) + assertJSONEquals(t, data, "", "null text marshal") +} + +func TestFloat32Pointer(t *testing.T) { + f := Float32From(1.2345) + ptr := f.Ptr() + if *ptr != 1.2345 { + t.Errorf("bad %s float32: %#v ≠ %v\n", "pointer", ptr, 1.2345) + } + + null := NewFloat32(0, false) + ptr = null.Ptr() + if ptr != nil { + t.Errorf("bad %s float32: %#v ≠ %s\n", "nil pointer", ptr, "nil") + } +} + +func TestFloat32IsZero(t *testing.T) { + f := Float32From(1.2345) + if f.IsZero() { + t.Errorf("IsZero() should be false") + } + + null := NewFloat32(0, false) + if !null.IsZero() { + t.Errorf("IsZero() should be true") + } + + zero := NewFloat32(0, true) + if zero.IsZero() { + t.Errorf("IsZero() should be false") + } +} + +func TestFloat32SetValid(t *testing.T) { + change := NewFloat32(0, false) + assertNullFloat32(t, change, "SetValid()") + change.SetValid(1.2345) + assertFloat32(t, change, "SetValid()") +} + +func TestFloat32Scan(t *testing.T) { + var f Float32 + err := f.Scan(1.2345) + maybePanic(err) + assertFloat32(t, f, "scanned float32") + + var null Float32 + err = null.Scan(nil) + maybePanic(err) + assertNullFloat32(t, null, "scanned null") +} + +func assertFloat32(t *testing.T, f Float32, from string) { + if f.Float32 != 1.2345 { + t.Errorf("bad %s float32: %f ≠ %f\n", from, f.Float32, 1.2345) + } + if !f.Valid { + t.Error(from, "is invalid, but should be valid") + } +} + +func assertNullFloat32(t *testing.T, f Float32, from string) { + if f.Valid { + t.Error(from, "is valid, but should be invalid") + } +} diff --git a/float.go b/float64.go similarity index 53% rename from float.go rename to float64.go index 1f57b95..416887d 100644 --- a/float.go +++ b/float64.go @@ -8,16 +8,16 @@ import ( "strconv" ) -// Float is a nullable float64. +// Float64 is a nullable float64. // It does not consider zero values to be null. // It will decode to null, not zero, if null. -type Float struct { +type Float64 struct { sql.NullFloat64 } -// NewFloat creates a new Float -func NewFloat(f float64, valid bool) Float { - return Float{ +// NewFloat64 creates a new Float64 +func NewFloat64(f float64, valid bool) Float64 { + return Float64{ NullFloat64: sql.NullFloat64{ Float64: f, Valid: valid, @@ -25,24 +25,24 @@ func NewFloat(f float64, valid bool) Float { } } -// FloatFrom creates a new Float that will always be valid. -func FloatFrom(f float64) Float { - return NewFloat(f, true) +// Float64From creates a new Float64 that will always be valid. +func Float64From(f float64) Float64 { + return NewFloat64(f, true) } -// FloatFromPtr creates a new Float that be null if f is nil. -func FloatFromPtr(f *float64) Float { +// Float64FromPtr creates a new Float64 that be null if f is nil. +func Float64FromPtr(f *float64) Float64 { if f == nil { - return NewFloat(0, false) + return NewFloat64(0, false) } - return NewFloat(*f, true) + return NewFloat64(*f, true) } // UnmarshalJSON implements json.Unmarshaler. // It supports number and null input. -// 0 will not be considered a null Float. +// 0 will not be considered a null Float64. // It also supports unmarshalling a sql.NullFloat64. -func (f *Float) UnmarshalJSON(data []byte) error { +func (f *Float64) UnmarshalJSON(data []byte) error { var err error var v interface{} if err = json.Unmarshal(data, &v); err != nil { @@ -57,16 +57,16 @@ func (f *Float) UnmarshalJSON(data []byte) error { f.Valid = false return nil default: - err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Float", reflect.TypeOf(v).Name()) + err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Float64", reflect.TypeOf(v).Name()) } f.Valid = err == nil return err } // UnmarshalText implements encoding.TextUnmarshaler. -// It will unmarshal to a null Float if the input is a blank or not an integer. +// It will unmarshal to a null Float64 if the input is a blank or not an integer. // It will return an error if the input is not an integer, blank, or "null". -func (f *Float) UnmarshalText(text []byte) error { +func (f *Float64) UnmarshalText(text []byte) error { str := string(text) if str == "" || str == "null" { f.Valid = false @@ -79,8 +79,8 @@ func (f *Float) UnmarshalText(text []byte) error { } // MarshalJSON implements json.Marshaler. -// It will encode null if this Float is null. -func (f Float) MarshalJSON() ([]byte, error) { +// It will encode null if this Float64 is null. +func (f Float64) MarshalJSON() ([]byte, error) { if !f.Valid { return []byte("null"), nil } @@ -88,30 +88,30 @@ func (f Float) MarshalJSON() ([]byte, error) { } // MarshalText implements encoding.TextMarshaler. -// It will encode a blank string if this Float is null. -func (f Float) MarshalText() ([]byte, error) { +// It will encode a blank string if this Float64 is null. +func (f Float64) MarshalText() ([]byte, error) { if !f.Valid { return []byte{}, nil } return []byte(strconv.FormatFloat(f.Float64, 'f', -1, 64)), nil } -// SetValid changes this Float's value and also sets it to be non-null. -func (f *Float) SetValid(n float64) { +// SetValid changes this Float64's value and also sets it to be non-null. +func (f *Float64) SetValid(n float64) { f.Float64 = n f.Valid = true } -// Ptr returns a pointer to this Float's value, or a nil pointer if this Float is null. -func (f Float) Ptr() *float64 { +// Ptr returns a pointer to this Float64's value, or a nil pointer if this Float64 is null. +func (f Float64) Ptr() *float64 { if !f.Valid { return nil } return &f.Float64 } -// IsZero returns true for invalid Floats, for future omitempty support (Go 1.4?) -// A non-null Float with a 0 value will not be considered zero. -func (f Float) IsZero() bool { +// IsZero returns true for invalid Float64s, for future omitempty support (Go 1.4?) +// A non-null Float64 with a 0 value will not be considered zero. +func (f Float64) IsZero() bool { return !f.Valid } diff --git a/float64_test.go b/float64_test.go new file mode 100644 index 0000000..1ddfbd4 --- /dev/null +++ b/float64_test.go @@ -0,0 +1,169 @@ +package null + +import ( + "encoding/json" + "testing" +) + +var ( + float64JSON = []byte(`1.2345`) + nullFloat64JSON = []byte(`{"Float64":1.2345,"Valid":true}`) +) + +func TestFloat64From(t *testing.T) { + f := Float64From(1.2345) + assertFloat64(t, f, "Float64From()") + + zero := Float64From(0) + if !zero.Valid { + t.Error("Float64From(0)", "is invalid, but should be valid") + } +} + +func TestFloat64FromPtr(t *testing.T) { + n := float64(1.2345) + iptr := &n + f := Float64FromPtr(iptr) + assertFloat64(t, f, "Float64FromPtr()") + + null := Float64FromPtr(nil) + assertNullFloat64(t, null, "Float64FromPtr(nil)") +} + +func TestUnmarshalFloat64(t *testing.T) { + var f Float64 + err := json.Unmarshal(float64JSON, &f) + maybePanic(err) + assertFloat64(t, f, "float64 json") + + var nf Float64 + err = json.Unmarshal(nullFloat64JSON, &nf) + maybePanic(err) + assertFloat64(t, nf, "sq.NullFloat64 json") + + var null Float64 + err = json.Unmarshal(nullJSON, &null) + maybePanic(err) + assertNullFloat64(t, null, "null json") + + var badType Float64 + err = json.Unmarshal(boolJSON, &badType) + if err == nil { + panic("err should not be nil") + } + assertNullFloat64(t, badType, "wrong type json") + + var invalid Float64 + err = invalid.UnmarshalJSON(invalidJSON) + if _, ok := err.(*json.SyntaxError); !ok { + t.Errorf("expected json.SyntaxError, not %T", err) + } +} + +func TestTextUnmarshalFloat64(t *testing.T) { + var f Float64 + err := f.UnmarshalText([]byte("1.2345")) + maybePanic(err) + assertFloat64(t, f, "UnmarshalText() float64") + + var blank Float64 + err = blank.UnmarshalText([]byte("")) + maybePanic(err) + assertNullFloat64(t, blank, "UnmarshalText() empty float64") + + var null Float64 + err = null.UnmarshalText([]byte("null")) + maybePanic(err) + assertNullFloat64(t, null, `UnmarshalText() "null"`) +} + +func TestMarshalFloat64(t *testing.T) { + f := Float64From(1.2345) + data, err := json.Marshal(f) + maybePanic(err) + assertJSONEquals(t, data, "1.2345", "non-empty json marshal") + + // invalid values should be encoded as null + null := NewFloat64(0, false) + data, err = json.Marshal(null) + maybePanic(err) + assertJSONEquals(t, data, "null", "null json marshal") +} + +func TestMarshalFloat64Text(t *testing.T) { + f := Float64From(1.2345) + data, err := f.MarshalText() + maybePanic(err) + assertJSONEquals(t, data, "1.2345", "non-empty text marshal") + + // invalid values should be encoded as null + null := NewFloat64(0, false) + data, err = null.MarshalText() + maybePanic(err) + assertJSONEquals(t, data, "", "null text marshal") +} + +func TestFloat64Pointer(t *testing.T) { + f := Float64From(1.2345) + ptr := f.Ptr() + if *ptr != 1.2345 { + t.Errorf("bad %s float64: %#v ≠ %v\n", "pointer", ptr, 1.2345) + } + + null := NewFloat64(0, false) + ptr = null.Ptr() + if ptr != nil { + t.Errorf("bad %s float64: %#v ≠ %s\n", "nil pointer", ptr, "nil") + } +} + +func TestFloat64IsZero(t *testing.T) { + f := Float64From(1.2345) + if f.IsZero() { + t.Errorf("IsZero() should be false") + } + + null := NewFloat64(0, false) + if !null.IsZero() { + t.Errorf("IsZero() should be true") + } + + zero := NewFloat64(0, true) + if zero.IsZero() { + t.Errorf("IsZero() should be false") + } +} + +func TestFloat64SetValid(t *testing.T) { + change := NewFloat64(0, false) + assertNullFloat64(t, change, "SetValid()") + change.SetValid(1.2345) + assertFloat64(t, change, "SetValid()") +} + +func TestFloat64Scan(t *testing.T) { + var f Float64 + err := f.Scan(1.2345) + maybePanic(err) + assertFloat64(t, f, "scanned float64") + + var null Float64 + err = null.Scan(nil) + maybePanic(err) + assertNullFloat64(t, null, "scanned null") +} + +func assertFloat64(t *testing.T, f Float64, from string) { + if f.Float64 != 1.2345 { + t.Errorf("bad %s float64: %f ≠ %f\n", from, f.Float64, 1.2345) + } + if !f.Valid { + t.Error(from, "is invalid, but should be valid") + } +} + +func assertNullFloat64(t *testing.T, f Float64, from string) { + if f.Valid { + t.Error(from, "is valid, but should be invalid") + } +} diff --git a/float_test.go b/float_test.go deleted file mode 100644 index abf1368..0000000 --- a/float_test.go +++ /dev/null @@ -1,169 +0,0 @@ -package null - -import ( - "encoding/json" - "testing" -) - -var ( - floatJSON = []byte(`1.2345`) - nullFloatJSON = []byte(`{"Float64":1.2345,"Valid":true}`) -) - -func TestFloatFrom(t *testing.T) { - f := FloatFrom(1.2345) - assertFloat(t, f, "FloatFrom()") - - zero := FloatFrom(0) - if !zero.Valid { - t.Error("FloatFrom(0)", "is invalid, but should be valid") - } -} - -func TestFloatFromPtr(t *testing.T) { - n := float64(1.2345) - iptr := &n - f := FloatFromPtr(iptr) - assertFloat(t, f, "FloatFromPtr()") - - null := FloatFromPtr(nil) - assertNullFloat(t, null, "FloatFromPtr(nil)") -} - -func TestUnmarshalFloat(t *testing.T) { - var f Float - err := json.Unmarshal(floatJSON, &f) - maybePanic(err) - assertFloat(t, f, "float json") - - var nf Float - err = json.Unmarshal(nullFloatJSON, &nf) - maybePanic(err) - assertFloat(t, nf, "sq.NullFloat64 json") - - var null Float - err = json.Unmarshal(nullJSON, &null) - maybePanic(err) - assertNullFloat(t, null, "null json") - - var badType Float - err = json.Unmarshal(boolJSON, &badType) - if err == nil { - panic("err should not be nil") - } - assertNullFloat(t, badType, "wrong type json") - - var invalid Float - err = invalid.UnmarshalJSON(invalidJSON) - if _, ok := err.(*json.SyntaxError); !ok { - t.Errorf("expected json.SyntaxError, not %T", err) - } -} - -func TestTextUnmarshalFloat(t *testing.T) { - var f Float - err := f.UnmarshalText([]byte("1.2345")) - maybePanic(err) - assertFloat(t, f, "UnmarshalText() float") - - var blank Float - err = blank.UnmarshalText([]byte("")) - maybePanic(err) - assertNullFloat(t, blank, "UnmarshalText() empty float") - - var null Float - err = null.UnmarshalText([]byte("null")) - maybePanic(err) - assertNullFloat(t, null, `UnmarshalText() "null"`) -} - -func TestMarshalFloat(t *testing.T) { - f := FloatFrom(1.2345) - data, err := json.Marshal(f) - maybePanic(err) - assertJSONEquals(t, data, "1.2345", "non-empty json marshal") - - // invalid values should be encoded as null - null := NewFloat(0, false) - data, err = json.Marshal(null) - maybePanic(err) - assertJSONEquals(t, data, "null", "null json marshal") -} - -func TestMarshalFloatText(t *testing.T) { - f := FloatFrom(1.2345) - data, err := f.MarshalText() - maybePanic(err) - assertJSONEquals(t, data, "1.2345", "non-empty text marshal") - - // invalid values should be encoded as null - null := NewFloat(0, false) - data, err = null.MarshalText() - maybePanic(err) - assertJSONEquals(t, data, "", "null text marshal") -} - -func TestFloatPointer(t *testing.T) { - f := FloatFrom(1.2345) - ptr := f.Ptr() - if *ptr != 1.2345 { - t.Errorf("bad %s float: %#v ≠ %v\n", "pointer", ptr, 1.2345) - } - - null := NewFloat(0, false) - ptr = null.Ptr() - if ptr != nil { - t.Errorf("bad %s float: %#v ≠ %s\n", "nil pointer", ptr, "nil") - } -} - -func TestFloatIsZero(t *testing.T) { - f := FloatFrom(1.2345) - if f.IsZero() { - t.Errorf("IsZero() should be false") - } - - null := NewFloat(0, false) - if !null.IsZero() { - t.Errorf("IsZero() should be true") - } - - zero := NewFloat(0, true) - if zero.IsZero() { - t.Errorf("IsZero() should be false") - } -} - -func TestFloatSetValid(t *testing.T) { - change := NewFloat(0, false) - assertNullFloat(t, change, "SetValid()") - change.SetValid(1.2345) - assertFloat(t, change, "SetValid()") -} - -func TestFloatScan(t *testing.T) { - var f Float - err := f.Scan(1.2345) - maybePanic(err) - assertFloat(t, f, "scanned float") - - var null Float - err = null.Scan(nil) - maybePanic(err) - assertNullFloat(t, null, "scanned null") -} - -func assertFloat(t *testing.T, f Float, from string) { - if f.Float64 != 1.2345 { - t.Errorf("bad %s float: %f ≠ %f\n", from, f.Float64, 1.2345) - } - if !f.Valid { - t.Error(from, "is invalid, but should be valid") - } -} - -func assertNullFloat(t *testing.T, f Float, from string) { - if f.Valid { - t.Error(from, "is valid, but should be invalid") - } -} diff --git a/int.go b/int.go index 981d17b..ca3d1dc 100644 --- a/int.go +++ b/int.go @@ -1,37 +1,43 @@ package null import ( - "database/sql" + "database/sql/driver" "encoding/json" "fmt" "reflect" "strconv" ) -// Int is an nullable int64. +// NullInt is a replica of sql.NullInt64 for int types. +type NullInt struct { + Int int + Valid bool +} + +// Int is an nullable int. // It does not consider zero values to be null. // It will decode to null, not zero, if null. type Int struct { - sql.NullInt64 + NullInt } // NewInt creates a new Int -func NewInt(i int64, valid bool) Int { +func NewInt(i int, valid bool) Int { return Int{ - NullInt64: sql.NullInt64{ - Int64: i, + NullInt: NullInt{ + Int: i, Valid: valid, }, } } // IntFrom creates a new Int that will always be valid. -func IntFrom(i int64) Int { +func IntFrom(i int) Int { return NewInt(i, true) } // IntFromPtr creates a new Int that be null if i is nil. -func IntFromPtr(i *int64) Int { +func IntFromPtr(i *int) Int { if i == nil { return NewInt(0, false) } @@ -41,7 +47,7 @@ func IntFromPtr(i *int64) Int { // UnmarshalJSON implements json.Unmarshaler. // It supports number and null input. // 0 will not be considered a null Int. -// It also supports unmarshalling a sql.NullInt64. +// It also supports unmarshalling a sql.NullInt. func (i *Int) UnmarshalJSON(data []byte) error { var err error var v interface{} @@ -50,10 +56,10 @@ func (i *Int) UnmarshalJSON(data []byte) error { } switch v.(type) { case float64: - // Unmarshal again, directly to int64, to avoid intermediate float64 - err = json.Unmarshal(data, &i.Int64) + // Unmarshal again, directly to int, to avoid intermediate float64 + err = json.Unmarshal(data, &i.Int) case map[string]interface{}: - err = json.Unmarshal(data, &i.NullInt64) + err = json.Unmarshal(data, &i.NullInt) case nil: i.Valid = false return nil @@ -74,8 +80,11 @@ func (i *Int) UnmarshalText(text []byte) error { return nil } var err error - i.Int64, err = strconv.ParseInt(string(text), 10, 64) + res, err := strconv.ParseInt(string(text), 10, 0) i.Valid = err == nil + if i.Valid { + i.Int = int(res) + } return err } @@ -85,7 +94,7 @@ func (i Int) MarshalJSON() ([]byte, error) { if !i.Valid { return []byte("null"), nil } - return []byte(strconv.FormatInt(i.Int64, 10)), nil + return []byte(strconv.FormatInt(int64(i.Int), 10)), nil } // MarshalText implements encoding.TextMarshaler. @@ -94,21 +103,21 @@ func (i Int) MarshalText() ([]byte, error) { if !i.Valid { return []byte{}, nil } - return []byte(strconv.FormatInt(i.Int64, 10)), nil + return []byte(strconv.FormatInt(int64(i.Int), 10)), nil } // SetValid changes this Int's value and also sets it to be non-null. -func (i *Int) SetValid(n int64) { - i.Int64 = n +func (i *Int) SetValid(n int) { + i.Int = n i.Valid = true } // Ptr returns a pointer to this Int's value, or a nil pointer if this Int is null. -func (i Int) Ptr() *int64 { +func (i Int) Ptr() *int { if !i.Valid { return nil } - return &i.Int64 + return &i.Int } // IsZero returns true for invalid Ints, for future omitempty support (Go 1.4?) @@ -116,3 +125,21 @@ func (i Int) Ptr() *int64 { func (i Int) IsZero() bool { return !i.Valid } + +// Scan implements the Scanner interface. +func (n *NullInt) Scan(value interface{}) error { + if value == nil { + n.Int, n.Valid = 0, false + return nil + } + n.Valid = true + return convertAssign(&n.Int, value) +} + +// Value implements the driver Valuer interface. +func (n NullInt) Value() (driver.Value, error) { + if !n.Valid { + return nil, nil + } + return n.Int, nil +} diff --git a/int16.go b/int16.go new file mode 100644 index 0000000..fe0d010 --- /dev/null +++ b/int16.go @@ -0,0 +1,145 @@ +package null + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "reflect" + "strconv" +) + +// NullInt16 is a replica of sql.NullInt64 for int16 types. +type NullInt16 struct { + Int16 int16 + Valid bool +} + +// Int16 is an nullable int16. +// It does not consider zero values to be null. +// It will decode to null, not zero, if null. +type Int16 struct { + NullInt16 +} + +// NewInt16 creates a new Int16 +func NewInt16(i int16, valid bool) Int16 { + return Int16{ + NullInt16: NullInt16{ + Int16: i, + Valid: valid, + }, + } +} + +// Int16From creates a new Int16 that will always be valid. +func Int16From(i int16) Int16 { + return NewInt16(i, true) +} + +// Int16FromPtr creates a new Int16 that be null if i is nil. +func Int16FromPtr(i *int16) Int16 { + if i == nil { + return NewInt16(0, false) + } + return NewInt16(*i, true) +} + +// UnmarshalJSON implements json.Unmarshaler. +// It supports number and null input. +// 0 will not be considered a null Int16. +// It also supports unmarshalling a sql.NullInt16. +func (i *Int16) UnmarshalJSON(data []byte) error { + var err error + var v interface{} + if err = json.Unmarshal(data, &v); err != nil { + return err + } + switch v.(type) { + case float64: + // Unmarshal again, directly to int16, to avoid intermediate float64 + err = json.Unmarshal(data, &i.Int16) + case map[string]interface{}: + err = json.Unmarshal(data, &i.NullInt16) + case nil: + i.Valid = false + return nil + default: + err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Int16", reflect.TypeOf(v).Name()) + } + i.Valid = err == nil + return err +} + +// UnmarshalText implements encoding.TextUnmarshaler. +// It will unmarshal to a null Int16 if the input is a blank or not an integer. +// It will return an error if the input is not an integer, blank, or "null". +func (i *Int16) UnmarshalText(text []byte) error { + str := string(text) + if str == "" || str == "null" { + i.Valid = false + return nil + } + var err error + res, err := strconv.ParseInt(string(text), 10, 16) + i.Valid = err == nil + if i.Valid { + i.Int16 = int16(res) + } + return err +} + +// MarshalJSON implements json.Marshaler. +// It will encode null if this Int16 is null. +func (i Int16) MarshalJSON() ([]byte, error) { + if !i.Valid { + return []byte("null"), nil + } + return []byte(strconv.FormatInt(int64(i.Int16), 10)), nil +} + +// MarshalText implements encoding.TextMarshaler. +// It will encode a blank string if this Int16 is null. +func (i Int16) MarshalText() ([]byte, error) { + if !i.Valid { + return []byte{}, nil + } + return []byte(strconv.FormatInt(int64(i.Int16), 10)), nil +} + +// SetValid changes this Int16's value and also sets it to be non-null. +func (i *Int16) SetValid(n int16) { + i.Int16 = n + i.Valid = true +} + +// Ptr returns a pointer to this Int16's value, or a nil pointer if this Int16 is null. +func (i Int16) Ptr() *int16 { + if !i.Valid { + return nil + } + return &i.Int16 +} + +// IsZero returns true for invalid Int16's, for future omitempty support (Go 1.4?) +// A non-null Int16 with a 0 value will not be considered zero. +func (i Int16) IsZero() bool { + return !i.Valid +} + +// Scan implements the Scanner interface. +func (n *NullInt16) Scan(value interface{}) error { + if value == nil { + n.Int16, n.Valid = 0, false + return nil + } + n.Valid = true + return convertAssign(&n.Int16, value) +} + +// Value implements the driver Valuer interface. +func (n NullInt16) Value() (driver.Value, error) { + if !n.Valid { + return nil, nil + } + return n.Int16, nil +} diff --git a/int16_test.go b/int16_test.go new file mode 100644 index 0000000..6bff793 --- /dev/null +++ b/int16_test.go @@ -0,0 +1,196 @@ +package null + +import ( + "encoding/json" + "math" + "strconv" + "testing" +) + +var ( + int16JSON = []byte(`32766`) + nullInt16JSON = []byte(`{"Int16":32766,"Valid":true}`) +) + +func TestInt16From(t *testing.T) { + i := Int16From(32766) + assertInt16(t, i, "Int16From()") + + zero := Int16From(0) + if !zero.Valid { + t.Error("Int16From(0)", "is invalid, but should be valid") + } +} + +func TestInt16FromPtr(t *testing.T) { + n := int16(32766) + iptr := &n + i := Int16FromPtr(iptr) + assertInt16(t, i, "Int16FromPtr()") + + null := Int16FromPtr(nil) + assertNullInt16(t, null, "Int16FromPtr(nil)") +} + +func TestUnmarshalInt16(t *testing.T) { + var i Int16 + err := json.Unmarshal(int16JSON, &i) + maybePanic(err) + assertInt16(t, i, "int16 json") + + var ni Int16 + err = json.Unmarshal(nullInt16JSON, &ni) + maybePanic(err) + assertInt16(t, ni, "sq.NullInt16 json") + + var null Int16 + err = json.Unmarshal(nullJSON, &null) + maybePanic(err) + assertNullInt16(t, null, "null json") + + var badType Int16 + err = json.Unmarshal(boolJSON, &badType) + if err == nil { + panic("err should not be nil") + } + assertNullInt16(t, badType, "wrong type json") + + var invalid Int16 + err = invalid.UnmarshalJSON(invalidJSON) + if _, ok := err.(*json.SyntaxError); !ok { + t.Errorf("expected json.SyntaxError, not %T", err) + } + assertNullInt16(t, invalid, "invalid json") +} + +func TestUnmarshalNonIntegerNumber16(t *testing.T) { + var i Int16 + err := json.Unmarshal(float64JSON, &i) + if err == nil { + panic("err should be present; non-integer number coerced to int16") + } +} + +func TestUnmarshalInt16Overflow(t *testing.T) { + int16Overflow := uint16(math.MaxInt16) + + // Max int16 should decode successfully + var i Int16 + err := json.Unmarshal([]byte(strconv.FormatUint(uint64(int16Overflow), 10)), &i) + maybePanic(err) + + // Attempt to overflow + int16Overflow++ + err = json.Unmarshal([]byte(strconv.FormatUint(uint64(int16Overflow), 10)), &i) + if err == nil { + panic("err should be present; decoded value overflows int16") + } +} + +func TestTextUnmarshalInt16(t *testing.T) { + var i Int16 + err := i.UnmarshalText([]byte("32766")) + maybePanic(err) + assertInt16(t, i, "UnmarshalText() int16") + + var blank Int16 + err = blank.UnmarshalText([]byte("")) + maybePanic(err) + assertNullInt16(t, blank, "UnmarshalText() empty int16") + + var null Int16 + err = null.UnmarshalText([]byte("null")) + maybePanic(err) + assertNullInt16(t, null, `UnmarshalText() "null"`) +} + +func TestMarshalInt16(t *testing.T) { + i := Int16From(32766) + data, err := json.Marshal(i) + maybePanic(err) + assertJSONEquals(t, data, "32766", "non-empty json marshal") + + // invalid values should be encoded as null + null := NewInt16(0, false) + data, err = json.Marshal(null) + maybePanic(err) + assertJSONEquals(t, data, "null", "null json marshal") +} + +func TestMarshalInt16Text(t *testing.T) { + i := Int16From(32766) + data, err := i.MarshalText() + maybePanic(err) + assertJSONEquals(t, data, "32766", "non-empty text marshal") + + // invalid values should be encoded as null + null := NewInt16(0, false) + data, err = null.MarshalText() + maybePanic(err) + assertJSONEquals(t, data, "", "null text marshal") +} + +func TestInt16Pointer(t *testing.T) { + i := Int16From(32766) + ptr := i.Ptr() + if *ptr != 32766 { + t.Errorf("bad %s int16: %#v ≠ %d\n", "pointer", ptr, 32766) + } + + null := NewInt16(0, false) + ptr = null.Ptr() + if ptr != nil { + t.Errorf("bad %s int16: %#v ≠ %s\n", "nil pointer", ptr, "nil") + } +} + +func TestInt16IsZero(t *testing.T) { + i := Int16From(32766) + if i.IsZero() { + t.Errorf("IsZero() should be false") + } + + null := NewInt16(0, false) + if !null.IsZero() { + t.Errorf("IsZero() should be true") + } + + zero := NewInt16(0, true) + if zero.IsZero() { + t.Errorf("IsZero() should be false") + } +} + +func TestInt16SetValid(t *testing.T) { + change := NewInt16(0, false) + assertNullInt16(t, change, "SetValid()") + change.SetValid(32766) + assertInt16(t, change, "SetValid()") +} + +func TestInt16Scan(t *testing.T) { + var i Int16 + err := i.Scan(32766) + maybePanic(err) + assertInt16(t, i, "scanned int16") + + var null Int16 + err = null.Scan(nil) + maybePanic(err) + assertNullInt16(t, null, "scanned null") +} + +func assertInt16(t *testing.T, i Int16, from string) { + if i.Int16 != 32766 { + t.Errorf("bad %s int16: %d ≠ %d\n", from, i.Int16, 32766) + } + if !i.Valid { + t.Error(from, "is invalid, but should be valid") + } +} + +func assertNullInt16(t *testing.T, i Int16, from string) { + if i.Valid { + t.Error(from, "is valid, but should be invalid") + } +} diff --git a/int32.go b/int32.go new file mode 100644 index 0000000..c28037a --- /dev/null +++ b/int32.go @@ -0,0 +1,145 @@ +package null + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "reflect" + "strconv" +) + +// NullInt32 is a replica of sql.NullInt64 for int32 types. +type NullInt32 struct { + Int32 int32 + Valid bool +} + +// Int32 is an nullable int32. +// It does not consider zero values to be null. +// It will decode to null, not zero, if null. +type Int32 struct { + NullInt32 +} + +// NewInt32 creates a new Int32 +func NewInt32(i int32, valid bool) Int32 { + return Int32{ + NullInt32: NullInt32{ + Int32: i, + Valid: valid, + }, + } +} + +// Int32From creates a new Int32 that will always be valid. +func Int32From(i int32) Int32 { + return NewInt32(i, true) +} + +// Int32FromPtr creates a new Int32 that be null if i is nil. +func Int32FromPtr(i *int32) Int32 { + if i == nil { + return NewInt32(0, false) + } + return NewInt32(*i, true) +} + +// UnmarshalJSON implements json.Unmarshaler. +// It supports number and null input. +// 0 will not be considered a null Int32. +// It also supports unmarshalling a sql.NullInt32. +func (i *Int32) UnmarshalJSON(data []byte) error { + var err error + var v interface{} + if err = json.Unmarshal(data, &v); err != nil { + return err + } + switch v.(type) { + case float64: + // Unmarshal again, directly to int32, to avoid intermediate float64 + err = json.Unmarshal(data, &i.Int32) + case map[string]interface{}: + err = json.Unmarshal(data, &i.NullInt32) + case nil: + i.Valid = false + return nil + default: + err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Int32", reflect.TypeOf(v).Name()) + } + i.Valid = err == nil + return err +} + +// UnmarshalText implements encoding.TextUnmarshaler. +// It will unmarshal to a null Int32 if the input is a blank or not an integer. +// It will return an error if the input is not an integer, blank, or "null". +func (i *Int32) UnmarshalText(text []byte) error { + str := string(text) + if str == "" || str == "null" { + i.Valid = false + return nil + } + var err error + res, err := strconv.ParseInt(string(text), 10, 32) + i.Valid = err == nil + if i.Valid { + i.Int32 = int32(res) + } + return err +} + +// MarshalJSON implements json.Marshaler. +// It will encode null if this Int32 is null. +func (i Int32) MarshalJSON() ([]byte, error) { + if !i.Valid { + return []byte("null"), nil + } + return []byte(strconv.FormatInt(int64(i.Int32), 10)), nil +} + +// MarshalText implements encoding.TextMarshaler. +// It will encode a blank string if this Int32 is null. +func (i Int32) MarshalText() ([]byte, error) { + if !i.Valid { + return []byte{}, nil + } + return []byte(strconv.FormatInt(int64(i.Int32), 10)), nil +} + +// SetValid changes this Int32's value and also sets it to be non-null. +func (i *Int32) SetValid(n int32) { + i.Int32 = n + i.Valid = true +} + +// Ptr returns a pointer to this Int32's value, or a nil pointer if this Int32 is null. +func (i Int32) Ptr() *int32 { + if !i.Valid { + return nil + } + return &i.Int32 +} + +// IsZero returns true for invalid Int32's, for future omitempty support (Go 1.4?) +// A non-null Int32 with a 0 value will not be considered zero. +func (i Int32) IsZero() bool { + return !i.Valid +} + +// Scan implements the Scanner interface. +func (n *NullInt32) Scan(value interface{}) error { + if value == nil { + n.Int32, n.Valid = 0, false + return nil + } + n.Valid = true + return convertAssign(&n.Int32, value) +} + +// Value implements the driver Valuer interface. +func (n NullInt32) Value() (driver.Value, error) { + if !n.Valid { + return nil, nil + } + return n.Int32, nil +} diff --git a/int32_test.go b/int32_test.go new file mode 100644 index 0000000..f687c21 --- /dev/null +++ b/int32_test.go @@ -0,0 +1,196 @@ +package null + +import ( + "encoding/json" + "math" + "strconv" + "testing" +) + +var ( + int32JSON = []byte(`2147483646`) + nullInt32JSON = []byte(`{"Int32":2147483646,"Valid":true}`) +) + +func TestInt32From(t *testing.T) { + i := Int32From(2147483646) + assertInt32(t, i, "Int32From()") + + zero := Int32From(0) + if !zero.Valid { + t.Error("Int32From(0)", "is invalid, but should be valid") + } +} + +func TestInt32FromPtr(t *testing.T) { + n := int32(2147483646) + iptr := &n + i := Int32FromPtr(iptr) + assertInt32(t, i, "Int32FromPtr()") + + null := Int32FromPtr(nil) + assertNullInt32(t, null, "Int32FromPtr(nil)") +} + +func TestUnmarshalInt32(t *testing.T) { + var i Int32 + err := json.Unmarshal(int32JSON, &i) + maybePanic(err) + assertInt32(t, i, "int32 json") + + var ni Int32 + err = json.Unmarshal(nullInt32JSON, &ni) + maybePanic(err) + assertInt32(t, ni, "sq.NullInt32 json") + + var null Int32 + err = json.Unmarshal(nullJSON, &null) + maybePanic(err) + assertNullInt32(t, null, "null json") + + var badType Int32 + err = json.Unmarshal(boolJSON, &badType) + if err == nil { + panic("err should not be nil") + } + assertNullInt32(t, badType, "wrong type json") + + var invalid Int32 + err = invalid.UnmarshalJSON(invalidJSON) + if _, ok := err.(*json.SyntaxError); !ok { + t.Errorf("expected json.SyntaxError, not %T", err) + } + assertNullInt32(t, invalid, "invalid json") +} + +func TestUnmarshalNonIntegerNumber32(t *testing.T) { + var i Int32 + err := json.Unmarshal(float64JSON, &i) + if err == nil { + panic("err should be present; non-integer number coerced to int32") + } +} + +func TestUnmarshalInt32Overflow(t *testing.T) { + int32Overflow := uint32(math.MaxInt32) + + // Max int32 should decode successfully + var i Int32 + err := json.Unmarshal([]byte(strconv.FormatUint(uint64(int32Overflow), 10)), &i) + maybePanic(err) + + // Attempt to overflow + int32Overflow++ + err = json.Unmarshal([]byte(strconv.FormatUint(uint64(int32Overflow), 10)), &i) + if err == nil { + panic("err should be present; decoded value overflows int32") + } +} + +func TestTextUnmarshalInt32(t *testing.T) { + var i Int32 + err := i.UnmarshalText([]byte("2147483646")) + maybePanic(err) + assertInt32(t, i, "UnmarshalText() int32") + + var blank Int32 + err = blank.UnmarshalText([]byte("")) + maybePanic(err) + assertNullInt32(t, blank, "UnmarshalText() empty int32") + + var null Int32 + err = null.UnmarshalText([]byte("null")) + maybePanic(err) + assertNullInt32(t, null, `UnmarshalText() "null"`) +} + +func TestMarshalInt32(t *testing.T) { + i := Int32From(2147483646) + data, err := json.Marshal(i) + maybePanic(err) + assertJSONEquals(t, data, "2147483646", "non-empty json marshal") + + // invalid values should be encoded as null + null := NewInt32(0, false) + data, err = json.Marshal(null) + maybePanic(err) + assertJSONEquals(t, data, "null", "null json marshal") +} + +func TestMarshalInt32Text(t *testing.T) { + i := Int32From(2147483646) + data, err := i.MarshalText() + maybePanic(err) + assertJSONEquals(t, data, "2147483646", "non-empty text marshal") + + // invalid values should be encoded as null + null := NewInt32(0, false) + data, err = null.MarshalText() + maybePanic(err) + assertJSONEquals(t, data, "", "null text marshal") +} + +func TestInt32Pointer(t *testing.T) { + i := Int32From(2147483646) + ptr := i.Ptr() + if *ptr != 2147483646 { + t.Errorf("bad %s int32: %#v ≠ %d\n", "pointer", ptr, 2147483646) + } + + null := NewInt32(0, false) + ptr = null.Ptr() + if ptr != nil { + t.Errorf("bad %s int32: %#v ≠ %s\n", "nil pointer", ptr, "nil") + } +} + +func TestInt32IsZero(t *testing.T) { + i := Int32From(2147483646) + if i.IsZero() { + t.Errorf("IsZero() should be false") + } + + null := NewInt32(0, false) + if !null.IsZero() { + t.Errorf("IsZero() should be true") + } + + zero := NewInt32(0, true) + if zero.IsZero() { + t.Errorf("IsZero() should be false") + } +} + +func TestInt32SetValid(t *testing.T) { + change := NewInt32(0, false) + assertNullInt32(t, change, "SetValid()") + change.SetValid(2147483646) + assertInt32(t, change, "SetValid()") +} + +func TestInt32Scan(t *testing.T) { + var i Int32 + err := i.Scan(2147483646) + maybePanic(err) + assertInt32(t, i, "scanned int32") + + var null Int32 + err = null.Scan(nil) + maybePanic(err) + assertNullInt32(t, null, "scanned null") +} + +func assertInt32(t *testing.T, i Int32, from string) { + if i.Int32 != 2147483646 { + t.Errorf("bad %s int32: %d ≠ %d\n", from, i.Int32, 2147483646) + } + if !i.Valid { + t.Error(from, "is invalid, but should be valid") + } +} + +func assertNullInt32(t *testing.T, i Int32, from string) { + if i.Valid { + t.Error(from, "is valid, but should be invalid") + } +} diff --git a/int64.go b/int64.go new file mode 100644 index 0000000..951900a --- /dev/null +++ b/int64.go @@ -0,0 +1,118 @@ +package null + +import ( + "database/sql" + "encoding/json" + "fmt" + "reflect" + "strconv" +) + +// Int64 is an nullable int64. +// It does not consider zero values to be null. +// It will decode to null, not zero, if null. +type Int64 struct { + sql.NullInt64 +} + +// NewInt64 creates a new Int64 +func NewInt64(i int64, valid bool) Int64 { + return Int64{ + NullInt64: sql.NullInt64{ + Int64: i, + Valid: valid, + }, + } +} + +// Int64From creates a new Int64 that will always be valid. +func Int64From(i int64) Int64 { + return NewInt64(i, true) +} + +// Int64FromPtr creates a new Int64 that be null if i is nil. +func Int64FromPtr(i *int64) Int64 { + if i == nil { + return NewInt64(0, false) + } + return NewInt64(*i, true) +} + +// UnmarshalJSON implements json.Unmarshaler. +// It supports number and null input. +// 0 will not be considered a null Int64. +// It also supports unmarshalling a sql.NullInt64. +func (i *Int64) UnmarshalJSON(data []byte) error { + var err error + var v interface{} + if err = json.Unmarshal(data, &v); err != nil { + return err + } + switch v.(type) { + case float64: + // Unmarshal again, directly to int64, to avoid intermediate float64 + err = json.Unmarshal(data, &i.Int64) + case map[string]interface{}: + err = json.Unmarshal(data, &i.NullInt64) + case nil: + i.Valid = false + return nil + default: + err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Int64", reflect.TypeOf(v).Name()) + } + i.Valid = err == nil + return err +} + +// UnmarshalText implements encoding.TextUnmarshaler. +// It will unmarshal to a null Int64 if the input is a blank or not an integer. +// It will return an error if the input is not an integer, blank, or "null". +func (i *Int64) UnmarshalText(text []byte) error { + str := string(text) + if str == "" || str == "null" { + i.Valid = false + return nil + } + var err error + i.Int64, err = strconv.ParseInt(string(text), 10, 64) + i.Valid = err == nil + return err +} + +// MarshalJSON implements json.Marshaler. +// It will encode null if this Int64 is null. +func (i Int64) MarshalJSON() ([]byte, error) { + if !i.Valid { + return []byte("null"), nil + } + return []byte(strconv.FormatInt(i.Int64, 10)), nil +} + +// MarshalText implements encoding.TextMarshaler. +// It will encode a blank string if this Int64 is null. +func (i Int64) MarshalText() ([]byte, error) { + if !i.Valid { + return []byte{}, nil + } + return []byte(strconv.FormatInt(i.Int64, 10)), nil +} + +// SetValid changes this Int64's value and also sets it to be non-null. +func (i *Int64) SetValid(n int64) { + i.Int64 = n + i.Valid = true +} + +// Ptr returns a pointer to this Int64's value, or a nil pointer if this Int64 is null. +func (i Int64) Ptr() *int64 { + if !i.Valid { + return nil + } + return &i.Int64 +} + +// IsZero returns true for invalid Int64's, for future omitempty support (Go 1.4?) +// A non-null Int64 with a 0 value will not be considered zero. +func (i Int64) IsZero() bool { + return !i.Valid +} diff --git a/int64_test.go b/int64_test.go new file mode 100644 index 0000000..4f61355 --- /dev/null +++ b/int64_test.go @@ -0,0 +1,196 @@ +package null + +import ( + "encoding/json" + "math" + "strconv" + "testing" +) + +var ( + int64JSON = []byte(`9223372036854775806`) + nullInt64JSON = []byte(`{"Int64":9223372036854775806,"Valid":true}`) +) + +func TestInt64From(t *testing.T) { + i := Int64From(9223372036854775806) + assertInt64(t, i, "Int64From()") + + zero := Int64From(0) + if !zero.Valid { + t.Error("Int64From(0)", "is invalid, but should be valid") + } +} + +func TestInt64FromPtr(t *testing.T) { + n := int64(9223372036854775806) + iptr := &n + i := Int64FromPtr(iptr) + assertInt64(t, i, "Int64FromPtr()") + + null := Int64FromPtr(nil) + assertNullInt64(t, null, "Int64FromPtr(nil)") +} + +func TestUnmarshalInt64(t *testing.T) { + var i Int64 + err := json.Unmarshal(int64JSON, &i) + maybePanic(err) + assertInt64(t, i, "int64 json") + + var ni Int64 + err = json.Unmarshal(nullInt64JSON, &ni) + maybePanic(err) + assertInt64(t, ni, "sq.NullInt64 json") + + var null Int64 + err = json.Unmarshal(nullJSON, &null) + maybePanic(err) + assertNullInt64(t, null, "null json") + + var badType Int64 + err = json.Unmarshal(boolJSON, &badType) + if err == nil { + panic("err should not be nil") + } + assertNullInt64(t, badType, "wrong type json") + + var invalid Int64 + err = invalid.UnmarshalJSON(invalidJSON) + if _, ok := err.(*json.SyntaxError); !ok { + t.Errorf("expected json.SyntaxError, not %T", err) + } + assertNullInt64(t, invalid, "invalid json") +} + +func TestUnmarshalNonIntegerNumber64(t *testing.T) { + var i Int64 + err := json.Unmarshal(float64JSON, &i) + if err == nil { + panic("err should be present; non-integer number coerced to int64") + } +} + +func TestUnmarshalInt64Overflow(t *testing.T) { + int64Overflow := uint64(math.MaxInt64) + + // Max int64 should decode successfully + var i Int64 + err := json.Unmarshal([]byte(strconv.FormatUint(uint64(int64Overflow), 10)), &i) + maybePanic(err) + + // Attempt to overflow + int64Overflow++ + err = json.Unmarshal([]byte(strconv.FormatUint(uint64(int64Overflow), 10)), &i) + if err == nil { + panic("err should be present; decoded value overflows int64") + } +} + +func TestTextUnmarshalInt64(t *testing.T) { + var i Int64 + err := i.UnmarshalText([]byte("9223372036854775806")) + maybePanic(err) + assertInt64(t, i, "UnmarshalText() int64") + + var blank Int64 + err = blank.UnmarshalText([]byte("")) + maybePanic(err) + assertNullInt64(t, blank, "UnmarshalText() empty int64") + + var null Int64 + err = null.UnmarshalText([]byte("null")) + maybePanic(err) + assertNullInt64(t, null, `UnmarshalText() "null"`) +} + +func TestMarshalInt64(t *testing.T) { + i := Int64From(9223372036854775806) + data, err := json.Marshal(i) + maybePanic(err) + assertJSONEquals(t, data, "9223372036854775806", "non-empty json marshal") + + // invalid values should be encoded as null + null := NewInt64(0, false) + data, err = json.Marshal(null) + maybePanic(err) + assertJSONEquals(t, data, "null", "null json marshal") +} + +func TestMarshalInt64Text(t *testing.T) { + i := Int64From(9223372036854775806) + data, err := i.MarshalText() + maybePanic(err) + assertJSONEquals(t, data, "9223372036854775806", "non-empty text marshal") + + // invalid values should be encoded as null + null := NewInt64(0, false) + data, err = null.MarshalText() + maybePanic(err) + assertJSONEquals(t, data, "", "null text marshal") +} + +func TestInt64Pointer(t *testing.T) { + i := Int64From(9223372036854775806) + ptr := i.Ptr() + if *ptr != 9223372036854775806 { + t.Errorf("bad %s int64: %#v ≠ %d\n", "pointer", ptr, 9223372036854775806) + } + + null := NewInt64(0, false) + ptr = null.Ptr() + if ptr != nil { + t.Errorf("bad %s int64: %#v ≠ %s\n", "nil pointer", ptr, "nil") + } +} + +func TestInt64IsZero(t *testing.T) { + i := Int64From(9223372036854775806) + if i.IsZero() { + t.Errorf("IsZero() should be false") + } + + null := NewInt64(0, false) + if !null.IsZero() { + t.Errorf("IsZero() should be true") + } + + zero := NewInt64(0, true) + if zero.IsZero() { + t.Errorf("IsZero() should be false") + } +} + +func TestInt64SetValid(t *testing.T) { + change := NewInt64(0, false) + assertNullInt64(t, change, "SetValid()") + change.SetValid(9223372036854775806) + assertInt64(t, change, "SetValid()") +} + +func TestInt64Scan(t *testing.T) { + var i Int64 + err := i.Scan(9223372036854775806) + maybePanic(err) + assertInt64(t, i, "scanned int64") + + var null Int64 + err = null.Scan(nil) + maybePanic(err) + assertNullInt64(t, null, "scanned null") +} + +func assertInt64(t *testing.T, i Int64, from string) { + if i.Int64 != 9223372036854775806 { + t.Errorf("bad %s int64: %d ≠ %d\n", from, i.Int64, 9223372036854775806) + } + if !i.Valid { + t.Error(from, "is invalid, but should be valid") + } +} + +func assertNullInt64(t *testing.T, i Int64, from string) { + if i.Valid { + t.Error(from, "is valid, but should be invalid") + } +} diff --git a/int8.go b/int8.go new file mode 100644 index 0000000..889eb3d --- /dev/null +++ b/int8.go @@ -0,0 +1,145 @@ +package null + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "reflect" + "strconv" +) + +// NullInt8 is a replica of sql.NullInt64 for int8 types. +type NullInt8 struct { + Int8 int8 + Valid bool +} + +// Int8 is an nullable int8. +// It does not consider zero values to be null. +// It will decode to null, not zero, if null. +type Int8 struct { + NullInt8 +} + +// NewInt8 creates a new Int8 +func NewInt8(i int8, valid bool) Int8 { + return Int8{ + NullInt8: NullInt8{ + Int8: i, + Valid: valid, + }, + } +} + +// Int8From creates a new Int8 that will always be valid. +func Int8From(i int8) Int8 { + return NewInt8(i, true) +} + +// Int8FromPtr creates a new Int8 that be null if i is nil. +func Int8FromPtr(i *int8) Int8 { + if i == nil { + return NewInt8(0, false) + } + return NewInt8(*i, true) +} + +// UnmarshalJSON implements json.Unmarshaler. +// It supports number and null input. +// 0 will not be considered a null Int8. +// It also supports unmarshalling a sql.NullInt8. +func (i *Int8) UnmarshalJSON(data []byte) error { + var err error + var v interface{} + if err = json.Unmarshal(data, &v); err != nil { + return err + } + switch v.(type) { + case float64: + // Unmarshal again, directly to int8, to avoid intermediate float64 + err = json.Unmarshal(data, &i.Int8) + case map[string]interface{}: + err = json.Unmarshal(data, &i.NullInt8) + case nil: + i.Valid = false + return nil + default: + err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Int8", reflect.TypeOf(v).Name()) + } + i.Valid = err == nil + return err +} + +// UnmarshalText implements encoding.TextUnmarshaler. +// It will unmarshal to a null Int8 if the input is a blank or not an integer. +// It will return an error if the input is not an integer, blank, or "null". +func (i *Int8) UnmarshalText(text []byte) error { + str := string(text) + if str == "" || str == "null" { + i.Valid = false + return nil + } + var err error + res, err := strconv.ParseInt(string(text), 10, 8) + i.Valid = err == nil + if i.Valid { + i.Int8 = int8(res) + } + return err +} + +// MarshalJSON implements json.Marshaler. +// It will encode null if this Int8 is null. +func (i Int8) MarshalJSON() ([]byte, error) { + if !i.Valid { + return []byte("null"), nil + } + return []byte(strconv.FormatInt(int64(i.Int8), 10)), nil +} + +// MarshalText implements encoding.TextMarshaler. +// It will encode a blank string if this Int8 is null. +func (i Int8) MarshalText() ([]byte, error) { + if !i.Valid { + return []byte{}, nil + } + return []byte(strconv.FormatInt(int64(i.Int8), 10)), nil +} + +// SetValid changes this Int8's value and also sets it to be non-null. +func (i *Int8) SetValid(n int8) { + i.Int8 = n + i.Valid = true +} + +// Ptr returns a pointer to this Int8's value, or a nil pointer if this Int8 is null. +func (i Int8) Ptr() *int8 { + if !i.Valid { + return nil + } + return &i.Int8 +} + +// IsZero returns true for invalid Int8's, for future omitempty support (Go 1.4?) +// A non-null Int8 with a 0 value will not be considered zero. +func (i Int8) IsZero() bool { + return !i.Valid +} + +// Scan implements the Scanner interface. +func (n *NullInt8) Scan(value interface{}) error { + if value == nil { + n.Int8, n.Valid = 0, false + return nil + } + n.Valid = true + return convertAssign(&n.Int8, value) +} + +// Value implements the driver Valuer interface. +func (n NullInt8) Value() (driver.Value, error) { + if !n.Valid { + return nil, nil + } + return n.Int8, nil +} diff --git a/int8_test.go b/int8_test.go new file mode 100644 index 0000000..fec44f9 --- /dev/null +++ b/int8_test.go @@ -0,0 +1,196 @@ +package null + +import ( + "encoding/json" + "math" + "strconv" + "testing" +) + +var ( + int8JSON = []byte(`126`) + nullInt8JSON = []byte(`{"Int8":126,"Valid":true}`) +) + +func TestInt8From(t *testing.T) { + i := Int8From(126) + assertInt8(t, i, "Int8From()") + + zero := Int8From(0) + if !zero.Valid { + t.Error("Int8From(0)", "is invalid, but should be valid") + } +} + +func TestInt8FromPtr(t *testing.T) { + n := int8(126) + iptr := &n + i := Int8FromPtr(iptr) + assertInt8(t, i, "Int8FromPtr()") + + null := Int8FromPtr(nil) + assertNullInt8(t, null, "Int8FromPtr(nil)") +} + +func TestUnmarshalInt8(t *testing.T) { + var i Int8 + err := json.Unmarshal(int8JSON, &i) + maybePanic(err) + assertInt8(t, i, "int8 json") + + var ni Int8 + err = json.Unmarshal(nullInt8JSON, &ni) + maybePanic(err) + assertInt8(t, ni, "sq.NullInt8 json") + + var null Int8 + err = json.Unmarshal(nullJSON, &null) + maybePanic(err) + assertNullInt8(t, null, "null json") + + var badType Int8 + err = json.Unmarshal(boolJSON, &badType) + if err == nil { + panic("err should not be nil") + } + assertNullInt8(t, badType, "wrong type json") + + var invalid Int8 + err = invalid.UnmarshalJSON(invalidJSON) + if _, ok := err.(*json.SyntaxError); !ok { + t.Errorf("expected json.SyntaxError, not %T", err) + } + assertNullInt8(t, invalid, "invalid json") +} + +func TestUnmarshalNonIntegerNumber8(t *testing.T) { + var i Int8 + err := json.Unmarshal(float64JSON, &i) + if err == nil { + panic("err should be present; non-integer number coerced to int8") + } +} + +func TestUnmarshalInt8Overflow(t *testing.T) { + int8Overflow := uint8(math.MaxInt8) + + // Max int8 should decode successfully + var i Int8 + err := json.Unmarshal([]byte(strconv.FormatUint(uint64(int8Overflow), 10)), &i) + maybePanic(err) + + // Attempt to overflow + int8Overflow++ + err = json.Unmarshal([]byte(strconv.FormatUint(uint64(int8Overflow), 10)), &i) + if err == nil { + panic("err should be present; decoded value overflows int8") + } +} + +func TestTextUnmarshalInt8(t *testing.T) { + var i Int8 + err := i.UnmarshalText([]byte("126")) + maybePanic(err) + assertInt8(t, i, "UnmarshalText() int8") + + var blank Int8 + err = blank.UnmarshalText([]byte("")) + maybePanic(err) + assertNullInt8(t, blank, "UnmarshalText() empty int8") + + var null Int8 + err = null.UnmarshalText([]byte("null")) + maybePanic(err) + assertNullInt8(t, null, `UnmarshalText() "null"`) +} + +func TestMarshalInt8(t *testing.T) { + i := Int8From(126) + data, err := json.Marshal(i) + maybePanic(err) + assertJSONEquals(t, data, "126", "non-empty json marshal") + + // invalid values should be encoded as null + null := NewInt8(0, false) + data, err = json.Marshal(null) + maybePanic(err) + assertJSONEquals(t, data, "null", "null json marshal") +} + +func TestMarshalInt8Text(t *testing.T) { + i := Int8From(126) + data, err := i.MarshalText() + maybePanic(err) + assertJSONEquals(t, data, "126", "non-empty text marshal") + + // invalid values should be encoded as null + null := NewInt8(0, false) + data, err = null.MarshalText() + maybePanic(err) + assertJSONEquals(t, data, "", "null text marshal") +} + +func TestInt8Pointer(t *testing.T) { + i := Int8From(126) + ptr := i.Ptr() + if *ptr != 126 { + t.Errorf("bad %s int8: %#v ≠ %d\n", "pointer", ptr, 126) + } + + null := NewInt8(0, false) + ptr = null.Ptr() + if ptr != nil { + t.Errorf("bad %s int8: %#v ≠ %s\n", "nil pointer", ptr, "nil") + } +} + +func TestInt8IsZero(t *testing.T) { + i := Int8From(126) + if i.IsZero() { + t.Errorf("IsZero() should be false") + } + + null := NewInt8(0, false) + if !null.IsZero() { + t.Errorf("IsZero() should be true") + } + + zero := NewInt8(0, true) + if zero.IsZero() { + t.Errorf("IsZero() should be false") + } +} + +func TestInt8SetValid(t *testing.T) { + change := NewInt8(0, false) + assertNullInt8(t, change, "SetValid()") + change.SetValid(126) + assertInt8(t, change, "SetValid()") +} + +func TestInt8Scan(t *testing.T) { + var i Int8 + err := i.Scan(126) + maybePanic(err) + assertInt8(t, i, "scanned int8") + + var null Int8 + err = null.Scan(nil) + maybePanic(err) + assertNullInt8(t, null, "scanned null") +} + +func assertInt8(t *testing.T, i Int8, from string) { + if i.Int8 != 126 { + t.Errorf("bad %s int8: %d ≠ %d\n", from, i.Int8, 126) + } + if !i.Valid { + t.Error(from, "is invalid, but should be valid") + } +} + +func assertNullInt8(t *testing.T, i Int8, from string) { + if i.Valid { + t.Error(from, "is valid, but should be invalid") + } +} diff --git a/int_test.go b/int_test.go index 2dd37a7..5a75aed 100644 --- a/int_test.go +++ b/int_test.go @@ -2,14 +2,12 @@ package null import ( "encoding/json" - "math" - "strconv" "testing" ) var ( intJSON = []byte(`12345`) - nullIntJSON = []byte(`{"Int64":12345,"Valid":true}`) + nullIntJSON = []byte(`{"Int":12345,"Valid":true}`) ) func TestIntFrom(t *testing.T) { @@ -23,7 +21,7 @@ func TestIntFrom(t *testing.T) { } func TestIntFromPtr(t *testing.T) { - n := int64(12345) + n := int(12345) iptr := &n i := IntFromPtr(iptr) assertInt(t, i, "IntFromPtr()") @@ -41,7 +39,7 @@ func TestUnmarshalInt(t *testing.T) { var ni Int err = json.Unmarshal(nullIntJSON, &ni) maybePanic(err) - assertInt(t, ni, "sq.NullInt64 json") + assertInt(t, ni, "sq.NullInt json") var null Int err = json.Unmarshal(nullJSON, &null) @@ -65,28 +63,12 @@ func TestUnmarshalInt(t *testing.T) { func TestUnmarshalNonIntegerNumber(t *testing.T) { var i Int - err := json.Unmarshal(floatJSON, &i) + err := json.Unmarshal(float64JSON, &i) if err == nil { panic("err should be present; non-integer number coerced to int") } } -func TestUnmarshalInt64Overflow(t *testing.T) { - int64Overflow := uint64(math.MaxInt64) - - // Max int64 should decode successfully - var i Int - err := json.Unmarshal([]byte(strconv.FormatUint(int64Overflow, 10)), &i) - maybePanic(err) - - // Attempt to overflow - int64Overflow++ - err = json.Unmarshal([]byte(strconv.FormatUint(int64Overflow, 10)), &i) - if err == nil { - panic("err should be present; decoded value overflows int64") - } -} - func TestTextUnmarshalInt(t *testing.T) { var i Int err := i.UnmarshalText([]byte("12345")) @@ -181,8 +163,8 @@ func TestIntScan(t *testing.T) { } func assertInt(t *testing.T, i Int, from string) { - if i.Int64 != 12345 { - t.Errorf("bad %s int: %d ≠ %d\n", from, i.Int64, 12345) + if i.Int != 12345 { + t.Errorf("bad %s int: %d ≠ %d\n", from, i.Int, 12345) } if !i.Valid { t.Error(from, "is invalid, but should be valid") diff --git a/uint.go b/uint.go new file mode 100644 index 0000000..2137297 --- /dev/null +++ b/uint.go @@ -0,0 +1,145 @@ +package null + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "reflect" + "strconv" +) + +// NullUint is a replica of sql.NullInt64 for uint types. +type NullUint struct { + Uint uint + Valid bool +} + +// Uint is an nullable uint. +// It does not consider zero values to be null. +// It will decode to null, not zero, if null. +type Uint struct { + NullUint +} + +// NewUint creates a new Uint +func NewUint(i uint, valid bool) Uint { + return Uint{ + NullUint: NullUint{ + Uint: i, + Valid: valid, + }, + } +} + +// UintFrom creates a new Uint that will always be valid. +func UintFrom(i uint) Uint { + return NewUint(i, true) +} + +// UintFromPtr creates a new Uint that be null if i is nil. +func UintFromPtr(i *uint) Uint { + if i == nil { + return NewUint(0, false) + } + return NewUint(*i, true) +} + +// UnmarshalJSON implements json.Unmarshaler. +// It supports number and null input. +// 0 will not be considered a null Uint. +// It also supports unmarshalling a sql.NullUint. +func (i *Uint) UnmarshalJSON(data []byte) error { + var err error + var v interface{} + if err = json.Unmarshal(data, &v); err != nil { + return err + } + switch v.(type) { + case float64: + // Unmarshal again, directly to uint, to avoid intermediate float64 + err = json.Unmarshal(data, &i.Uint) + case map[string]interface{}: + err = json.Unmarshal(data, &i.NullUint) + case nil: + i.Valid = false + return nil + default: + err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Uint", reflect.TypeOf(v).Name()) + } + i.Valid = err == nil + return err +} + +// UnmarshalText implements encoding.TextUnmarshaler. +// It will unmarshal to a null Uint if the input is a blank or not an integer. +// It will return an error if the input is not an integer, blank, or "null". +func (i *Uint) UnmarshalText(text []byte) error { + str := string(text) + if str == "" || str == "null" { + i.Valid = false + return nil + } + var err error + res, err := strconv.ParseUint(string(text), 10, 0) + i.Valid = err == nil + if i.Valid { + i.Uint = uint(res) + } + return err +} + +// MarshalJSON implements json.Marshaler. +// It will encode null if this Uint is null. +func (i Uint) MarshalJSON() ([]byte, error) { + if !i.Valid { + return []byte("null"), nil + } + return []byte(strconv.FormatUint(uint64(i.Uint), 10)), nil +} + +// MarshalText implements encoding.TextMarshaler. +// It will encode a blank string if this Uint is null. +func (i Uint) MarshalText() ([]byte, error) { + if !i.Valid { + return []byte{}, nil + } + return []byte(strconv.FormatUint(uint64(i.Uint), 10)), nil +} + +// SetValid changes this Uint's value and also sets it to be non-null. +func (i *Uint) SetValid(n uint) { + i.Uint = n + i.Valid = true +} + +// Ptr returns a pointer to this Uint's value, or a nil pointer if this Uint is null. +func (i Uint) Ptr() *uint { + if !i.Valid { + return nil + } + return &i.Uint +} + +// IsZero returns true for invalid Uints, for future omitempty support (Go 1.4?) +// A non-null Uint with a 0 value will not be considered zero. +func (i Uint) IsZero() bool { + return !i.Valid +} + +// Scan implements the Scanner interface. +func (n *NullUint) Scan(value interface{}) error { + if value == nil { + n.Uint, n.Valid = 0, false + return nil + } + n.Valid = true + return convertAssign(&n.Uint, value) +} + +// Value implements the driver Valuer interface. +func (n NullUint) Value() (driver.Value, error) { + if !n.Valid { + return nil, nil + } + return n.Uint, nil +} diff --git a/uint16.go b/uint16.go new file mode 100644 index 0000000..317e371 --- /dev/null +++ b/uint16.go @@ -0,0 +1,145 @@ +package null + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "reflect" + "strconv" +) + +// NullUint16 is a replica of sql.NullInt64 for uint16 types. +type NullUint16 struct { + Uint16 uint16 + Valid bool +} + +// Uint16 is an nullable uint16. +// It does not consider zero values to be null. +// It will decode to null, not zero, if null. +type Uint16 struct { + NullUint16 +} + +// NewUint16 creates a new Uint16 +func NewUint16(i uint16, valid bool) Uint16 { + return Uint16{ + NullUint16: NullUint16{ + Uint16: i, + Valid: valid, + }, + } +} + +// Uint16From creates a new Uint16 that will always be valid. +func Uint16From(i uint16) Uint16 { + return NewUint16(i, true) +} + +// Uint16FromPtr creates a new Uint16 that be null if i is nil. +func Uint16FromPtr(i *uint16) Uint16 { + if i == nil { + return NewUint16(0, false) + } + return NewUint16(*i, true) +} + +// UnmarshalJSON implements json.Unmarshaler. +// It supports number and null input. +// 0 will not be considered a null Uint16. +// It also supports unmarshalling a sql.NullUint16. +func (i *Uint16) UnmarshalJSON(data []byte) error { + var err error + var v interface{} + if err = json.Unmarshal(data, &v); err != nil { + return err + } + switch v.(type) { + case float64: + // Unmarshal again, directly to uint16, to avoid intermediate float64 + err = json.Unmarshal(data, &i.Uint16) + case map[string]interface{}: + err = json.Unmarshal(data, &i.NullUint16) + case nil: + i.Valid = false + return nil + default: + err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Uint16", reflect.TypeOf(v).Name()) + } + i.Valid = err == nil + return err +} + +// UnmarshalText implements encoding.TextUnmarshaler. +// It will unmarshal to a null Uint16 if the input is a blank or not an integer. +// It will return an error if the input is not an integer, blank, or "null". +func (i *Uint16) UnmarshalText(text []byte) error { + str := string(text) + if str == "" || str == "null" { + i.Valid = false + return nil + } + var err error + res, err := strconv.ParseUint(string(text), 10, 16) + i.Valid = err == nil + if i.Valid { + i.Uint16 = uint16(res) + } + return err +} + +// MarshalJSON implements json.Marshaler. +// It will encode null if this Uint16 is null. +func (i Uint16) MarshalJSON() ([]byte, error) { + if !i.Valid { + return []byte("null"), nil + } + return []byte(strconv.FormatUint(uint64(i.Uint16), 10)), nil +} + +// MarshalText implements encoding.TextMarshaler. +// It will encode a blank string if this Uint16 is null. +func (i Uint16) MarshalText() ([]byte, error) { + if !i.Valid { + return []byte{}, nil + } + return []byte(strconv.FormatUint(uint64(i.Uint16), 10)), nil +} + +// SetValid changes this Uint16's value and also sets it to be non-null. +func (i *Uint16) SetValid(n uint16) { + i.Uint16 = n + i.Valid = true +} + +// Ptr returns a pointer to this Uint16's value, or a nil pointer if this Uint16 is null. +func (i Uint16) Ptr() *uint16 { + if !i.Valid { + return nil + } + return &i.Uint16 +} + +// IsZero returns true for invalid Uint16's, for future omitempty support (Go 1.4?) +// A non-null Uint16 with a 0 value will not be considered zero. +func (i Uint16) IsZero() bool { + return !i.Valid +} + +// Scan implements the Scanner interface. +func (n *NullUint16) Scan(value interface{}) error { + if value == nil { + n.Uint16, n.Valid = 0, false + return nil + } + n.Valid = true + return convertAssign(&n.Uint16, value) +} + +// Value implements the driver Valuer interface. +func (n NullUint16) Value() (driver.Value, error) { + if !n.Valid { + return nil, nil + } + return n.Uint16, nil +} diff --git a/uint16_test.go b/uint16_test.go new file mode 100644 index 0000000..02366be --- /dev/null +++ b/uint16_test.go @@ -0,0 +1,196 @@ +package null + +import ( + "encoding/json" + "math" + "strconv" + "testing" +) + +var ( + uint16JSON = []byte(`65534`) + nullUint16JSON = []byte(`{"Uint16":65534,"Valid":true}`) +) + +func TestUint16From(t *testing.T) { + i := Uint16From(65534) + assertUint16(t, i, "Uint16From()") + + zero := Uint16From(0) + if !zero.Valid { + t.Error("Uint16From(0)", "is invalid, but should be valid") + } +} + +func TestUint16FromPtr(t *testing.T) { + n := uint16(65534) + iptr := &n + i := Uint16FromPtr(iptr) + assertUint16(t, i, "Uint16FromPtr()") + + null := Uint16FromPtr(nil) + assertNullUint16(t, null, "Uint16FromPtr(nil)") +} + +func TestUnmarshalUint16(t *testing.T) { + var i Uint16 + err := json.Unmarshal(uint16JSON, &i) + maybePanic(err) + assertUint16(t, i, "uint16 json") + + var ni Uint16 + err = json.Unmarshal(nullUint16JSON, &ni) + maybePanic(err) + assertUint16(t, ni, "sq.NullUint16 json") + + var null Uint16 + err = json.Unmarshal(nullJSON, &null) + maybePanic(err) + assertNullUint16(t, null, "null json") + + var badType Uint16 + err = json.Unmarshal(boolJSON, &badType) + if err == nil { + panic("err should not be nil") + } + assertNullUint16(t, badType, "wrong type json") + + var invalid Uint16 + err = invalid.UnmarshalJSON(invalidJSON) + if _, ok := err.(*json.SyntaxError); !ok { + t.Errorf("expected json.SyntaxError, not %T", err) + } + assertNullUint16(t, invalid, "invalid json") +} + +func TestUnmarshalNonUintegerNumber16(t *testing.T) { + var i Uint16 + err := json.Unmarshal(float64JSON, &i) + if err == nil { + panic("err should be present; non-integer number coerced to uint16") + } +} + +func TestUnmarshalUint16Overflow(t *testing.T) { + uint16Overflow := int64(math.MaxUint16) + + // Max uint16 should decode successfully + var i Uint16 + err := json.Unmarshal([]byte(strconv.FormatUint(uint64(uint16Overflow), 10)), &i) + maybePanic(err) + + // Attempt to overflow + uint16Overflow++ + err = json.Unmarshal([]byte(strconv.FormatUint(uint64(uint16Overflow), 10)), &i) + if err == nil { + panic("err should be present; decoded value overflows uint16") + } +} + +func TestTextUnmarshalUint16(t *testing.T) { + var i Uint16 + err := i.UnmarshalText([]byte("65534")) + maybePanic(err) + assertUint16(t, i, "UnmarshalText() uint16") + + var blank Uint16 + err = blank.UnmarshalText([]byte("")) + maybePanic(err) + assertNullUint16(t, blank, "UnmarshalText() empty uint16") + + var null Uint16 + err = null.UnmarshalText([]byte("null")) + maybePanic(err) + assertNullUint16(t, null, `UnmarshalText() "null"`) +} + +func TestMarshalUint16(t *testing.T) { + i := Uint16From(65534) + data, err := json.Marshal(i) + maybePanic(err) + assertJSONEquals(t, data, "65534", "non-empty json marshal") + + // invalid values should be encoded as null + null := NewUint16(0, false) + data, err = json.Marshal(null) + maybePanic(err) + assertJSONEquals(t, data, "null", "null json marshal") +} + +func TestMarshalUint16Text(t *testing.T) { + i := Uint16From(65534) + data, err := i.MarshalText() + maybePanic(err) + assertJSONEquals(t, data, "65534", "non-empty text marshal") + + // invalid values should be encoded as null + null := NewUint16(0, false) + data, err = null.MarshalText() + maybePanic(err) + assertJSONEquals(t, data, "", "null text marshal") +} + +func TestUint16Pointer(t *testing.T) { + i := Uint16From(65534) + ptr := i.Ptr() + if *ptr != 65534 { + t.Errorf("bad %s uint16: %#v ≠ %d\n", "pointer", ptr, 65534) + } + + null := NewUint16(0, false) + ptr = null.Ptr() + if ptr != nil { + t.Errorf("bad %s uint16: %#v ≠ %s\n", "nil pointer", ptr, "nil") + } +} + +func TestUint16IsZero(t *testing.T) { + i := Uint16From(65534) + if i.IsZero() { + t.Errorf("IsZero() should be false") + } + + null := NewUint16(0, false) + if !null.IsZero() { + t.Errorf("IsZero() should be true") + } + + zero := NewUint16(0, true) + if zero.IsZero() { + t.Errorf("IsZero() should be false") + } +} + +func TestUint16SetValid(t *testing.T) { + change := NewUint16(0, false) + assertNullUint16(t, change, "SetValid()") + change.SetValid(65534) + assertUint16(t, change, "SetValid()") +} + +func TestUint16Scan(t *testing.T) { + var i Uint16 + err := i.Scan(65534) + maybePanic(err) + assertUint16(t, i, "scanned uint16") + + var null Uint16 + err = null.Scan(nil) + maybePanic(err) + assertNullUint16(t, null, "scanned null") +} + +func assertUint16(t *testing.T, i Uint16, from string) { + if i.Uint16 != 65534 { + t.Errorf("bad %s uint16: %d ≠ %d\n", from, i.Uint16, 65534) + } + if !i.Valid { + t.Error(from, "is invalid, but should be valid") + } +} + +func assertNullUint16(t *testing.T, i Uint16, from string) { + if i.Valid { + t.Error(from, "is valid, but should be invalid") + } +} diff --git a/uint32.go b/uint32.go new file mode 100644 index 0000000..ee42920 --- /dev/null +++ b/uint32.go @@ -0,0 +1,145 @@ +package null + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "reflect" + "strconv" +) + +// NullUint32 is a replica of sql.NullInt64 for uint32 types. +type NullUint32 struct { + Uint32 uint32 + Valid bool +} + +// Uint32 is an nullable uint32. +// It does not consider zero values to be null. +// It will decode to null, not zero, if null. +type Uint32 struct { + NullUint32 +} + +// NewUint32 creates a new Uint32 +func NewUint32(i uint32, valid bool) Uint32 { + return Uint32{ + NullUint32: NullUint32{ + Uint32: i, + Valid: valid, + }, + } +} + +// Uint32From creates a new Uint32 that will always be valid. +func Uint32From(i uint32) Uint32 { + return NewUint32(i, true) +} + +// Uint32FromPtr creates a new Uint32 that be null if i is nil. +func Uint32FromPtr(i *uint32) Uint32 { + if i == nil { + return NewUint32(0, false) + } + return NewUint32(*i, true) +} + +// UnmarshalJSON implements json.Unmarshaler. +// It supports number and null input. +// 0 will not be considered a null Uint32. +// It also supports unmarshalling a sql.NullUint32. +func (i *Uint32) UnmarshalJSON(data []byte) error { + var err error + var v interface{} + if err = json.Unmarshal(data, &v); err != nil { + return err + } + switch v.(type) { + case float64: + // Unmarshal again, directly to uint32, to avoid intermediate float64 + err = json.Unmarshal(data, &i.Uint32) + case map[string]interface{}: + err = json.Unmarshal(data, &i.NullUint32) + case nil: + i.Valid = false + return nil + default: + err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Uint32", reflect.TypeOf(v).Name()) + } + i.Valid = err == nil + return err +} + +// UnmarshalText implements encoding.TextUnmarshaler. +// It will unmarshal to a null Uint32 if the input is a blank or not an integer. +// It will return an error if the input is not an integer, blank, or "null". +func (i *Uint32) UnmarshalText(text []byte) error { + str := string(text) + if str == "" || str == "null" { + i.Valid = false + return nil + } + var err error + res, err := strconv.ParseUint(string(text), 10, 32) + i.Valid = err == nil + if i.Valid { + i.Uint32 = uint32(res) + } + return err +} + +// MarshalJSON implements json.Marshaler. +// It will encode null if this Uint32 is null. +func (i Uint32) MarshalJSON() ([]byte, error) { + if !i.Valid { + return []byte("null"), nil + } + return []byte(strconv.FormatUint(uint64(i.Uint32), 10)), nil +} + +// MarshalText implements encoding.TextMarshaler. +// It will encode a blank string if this Uint32 is null. +func (i Uint32) MarshalText() ([]byte, error) { + if !i.Valid { + return []byte{}, nil + } + return []byte(strconv.FormatUint(uint64(i.Uint32), 10)), nil +} + +// SetValid changes this Uint32's value and also sets it to be non-null. +func (i *Uint32) SetValid(n uint32) { + i.Uint32 = n + i.Valid = true +} + +// Ptr returns a pointer to this Uint32's value, or a nil pointer if this Uint32 is null. +func (i Uint32) Ptr() *uint32 { + if !i.Valid { + return nil + } + return &i.Uint32 +} + +// IsZero returns true for invalid Uint32's, for future omitempty support (Go 1.4?) +// A non-null Uint32 with a 0 value will not be considered zero. +func (i Uint32) IsZero() bool { + return !i.Valid +} + +// Scan implements the Scanner interface. +func (n *NullUint32) Scan(value interface{}) error { + if value == nil { + n.Uint32, n.Valid = 0, false + return nil + } + n.Valid = true + return convertAssign(&n.Uint32, value) +} + +// Value implements the driver Valuer interface. +func (n NullUint32) Value() (driver.Value, error) { + if !n.Valid { + return nil, nil + } + return n.Uint32, nil +} diff --git a/uint32_test.go b/uint32_test.go new file mode 100644 index 0000000..ec448c0 --- /dev/null +++ b/uint32_test.go @@ -0,0 +1,196 @@ +package null + +import ( + "encoding/json" + "math" + "strconv" + "testing" +) + +var ( + uint32JSON = []byte(`4294967294`) + nullUint32JSON = []byte(`{"Uint32":4294967294,"Valid":true}`) +) + +func TestUint32From(t *testing.T) { + i := Uint32From(4294967294) + assertUint32(t, i, "Uint32From()") + + zero := Uint32From(0) + if !zero.Valid { + t.Error("Uint32From(0)", "is invalid, but should be valid") + } +} + +func TestUint32FromPtr(t *testing.T) { + n := uint32(4294967294) + iptr := &n + i := Uint32FromPtr(iptr) + assertUint32(t, i, "Uint32FromPtr()") + + null := Uint32FromPtr(nil) + assertNullUint32(t, null, "Uint32FromPtr(nil)") +} + +func TestUnmarshalUint32(t *testing.T) { + var i Uint32 + err := json.Unmarshal(uint32JSON, &i) + maybePanic(err) + assertUint32(t, i, "uint32 json") + + var ni Uint32 + err = json.Unmarshal(nullUint32JSON, &ni) + maybePanic(err) + assertUint32(t, ni, "sq.NullUint32 json") + + var null Uint32 + err = json.Unmarshal(nullJSON, &null) + maybePanic(err) + assertNullUint32(t, null, "null json") + + var badType Uint32 + err = json.Unmarshal(boolJSON, &badType) + if err == nil { + panic("err should not be nil") + } + assertNullUint32(t, badType, "wrong type json") + + var invalid Uint32 + err = invalid.UnmarshalJSON(invalidJSON) + if _, ok := err.(*json.SyntaxError); !ok { + t.Errorf("expected json.SyntaxError, not %T", err) + } + assertNullUint32(t, invalid, "invalid json") +} + +func TestUnmarshalNonUintegerNumber32(t *testing.T) { + var i Uint32 + err := json.Unmarshal(float64JSON, &i) + if err == nil { + panic("err should be present; non-integer number coerced to uint32") + } +} + +func TestUnmarshalUint32Overflow(t *testing.T) { + uint32Overflow := int64(math.MaxUint32) + + // Max uint32 should decode successfully + var i Uint32 + err := json.Unmarshal([]byte(strconv.FormatUint(uint64(uint32Overflow), 10)), &i) + maybePanic(err) + + // Attempt to overflow + uint32Overflow++ + err = json.Unmarshal([]byte(strconv.FormatUint(uint64(uint32Overflow), 10)), &i) + if err == nil { + panic("err should be present; decoded value overflows uint32") + } +} + +func TestTextUnmarshalUint32(t *testing.T) { + var i Uint32 + err := i.UnmarshalText([]byte("4294967294")) + maybePanic(err) + assertUint32(t, i, "UnmarshalText() uint32") + + var blank Uint32 + err = blank.UnmarshalText([]byte("")) + maybePanic(err) + assertNullUint32(t, blank, "UnmarshalText() empty uint32") + + var null Uint32 + err = null.UnmarshalText([]byte("null")) + maybePanic(err) + assertNullUint32(t, null, `UnmarshalText() "null"`) +} + +func TestMarshalUint32(t *testing.T) { + i := Uint32From(4294967294) + data, err := json.Marshal(i) + maybePanic(err) + assertJSONEquals(t, data, "4294967294", "non-empty json marshal") + + // invalid values should be encoded as null + null := NewUint32(0, false) + data, err = json.Marshal(null) + maybePanic(err) + assertJSONEquals(t, data, "null", "null json marshal") +} + +func TestMarshalUint32Text(t *testing.T) { + i := Uint32From(4294967294) + data, err := i.MarshalText() + maybePanic(err) + assertJSONEquals(t, data, "4294967294", "non-empty text marshal") + + // invalid values should be encoded as null + null := NewUint32(0, false) + data, err = null.MarshalText() + maybePanic(err) + assertJSONEquals(t, data, "", "null text marshal") +} + +func TestUint32Pointer(t *testing.T) { + i := Uint32From(4294967294) + ptr := i.Ptr() + if *ptr != 4294967294 { + t.Errorf("bad %s uint32: %#v ≠ %d\n", "pointer", ptr, 4294967294) + } + + null := NewUint32(0, false) + ptr = null.Ptr() + if ptr != nil { + t.Errorf("bad %s uint32: %#v ≠ %s\n", "nil pointer", ptr, "nil") + } +} + +func TestUint32IsZero(t *testing.T) { + i := Uint32From(4294967294) + if i.IsZero() { + t.Errorf("IsZero() should be false") + } + + null := NewUint32(0, false) + if !null.IsZero() { + t.Errorf("IsZero() should be true") + } + + zero := NewUint32(0, true) + if zero.IsZero() { + t.Errorf("IsZero() should be false") + } +} + +func TestUint32SetValid(t *testing.T) { + change := NewUint32(0, false) + assertNullUint32(t, change, "SetValid()") + change.SetValid(4294967294) + assertUint32(t, change, "SetValid()") +} + +func TestUint32Scan(t *testing.T) { + var i Uint32 + err := i.Scan(4294967294) + maybePanic(err) + assertUint32(t, i, "scanned uint32") + + var null Uint32 + err = null.Scan(nil) + maybePanic(err) + assertNullUint32(t, null, "scanned null") +} + +func assertUint32(t *testing.T, i Uint32, from string) { + if i.Uint32 != 4294967294 { + t.Errorf("bad %s uint32: %d ≠ %d\n", from, i.Uint32, 4294967294) + } + if !i.Valid { + t.Error(from, "is invalid, but should be valid") + } +} + +func assertNullUint32(t *testing.T, i Uint32, from string) { + if i.Valid { + t.Error(from, "is valid, but should be invalid") + } +} diff --git a/uint64.go b/uint64.go new file mode 100644 index 0000000..ec5ee79 --- /dev/null +++ b/uint64.go @@ -0,0 +1,145 @@ +package null + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "reflect" + "strconv" +) + +// NullUint64 is a replica of sql.NullInt64 for uint64 types. +type NullUint64 struct { + Uint64 uint64 + Valid bool +} + +// Uint64 is an nullable uint64. +// It does not consider zero values to be null. +// It will decode to null, not zero, if null. +type Uint64 struct { + NullUint64 +} + +// NewUint64 creates a new Uint64 +func NewUint64(i uint64, valid bool) Uint64 { + return Uint64{ + NullUint64: NullUint64{ + Uint64: i, + Valid: valid, + }, + } +} + +// Uint64From creates a new Uint64 that will always be valid. +func Uint64From(i uint64) Uint64 { + return NewUint64(i, true) +} + +// Uint64FromPtr creates a new Uint64 that be null if i is nil. +func Uint64FromPtr(i *uint64) Uint64 { + if i == nil { + return NewUint64(0, false) + } + return NewUint64(*i, true) +} + +// UnmarshalJSON implements json.Unmarshaler. +// It supports number and null input. +// 0 will not be considered a null Uint64. +// It also supports unmarshalling a sql.NullUint64. +func (i *Uint64) UnmarshalJSON(data []byte) error { + var err error + var v interface{} + if err = json.Unmarshal(data, &v); err != nil { + return err + } + switch v.(type) { + case float64: + // Unmarshal again, directly to uint64, to avoid intermediate float64 + err = json.Unmarshal(data, &i.Uint64) + case map[string]interface{}: + err = json.Unmarshal(data, &i.NullUint64) + case nil: + i.Valid = false + return nil + default: + err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Uint64", reflect.TypeOf(v).Name()) + } + i.Valid = err == nil + return err +} + +// UnmarshalText implements encoding.TextUnmarshaler. +// It will unmarshal to a null Uint64 if the input is a blank or not an integer. +// It will return an error if the input is not an integer, blank, or "null". +func (i *Uint64) UnmarshalText(text []byte) error { + str := string(text) + if str == "" || str == "null" { + i.Valid = false + return nil + } + var err error + res, err := strconv.ParseUint(string(text), 10, 64) + i.Valid = err == nil + if i.Valid { + i.Uint64 = uint64(res) + } + return err +} + +// MarshalJSON implements json.Marshaler. +// It will encode null if this Uint64 is null. +func (i Uint64) MarshalJSON() ([]byte, error) { + if !i.Valid { + return []byte("null"), nil + } + return []byte(strconv.FormatUint(i.Uint64, 10)), nil +} + +// MarshalText implements encoding.TextMarshaler. +// It will encode a blank string if this Uint64 is null. +func (i Uint64) MarshalText() ([]byte, error) { + if !i.Valid { + return []byte{}, nil + } + return []byte(strconv.FormatUint(i.Uint64, 10)), nil +} + +// SetValid changes this Uint64's value and also sets it to be non-null. +func (i *Uint64) SetValid(n uint64) { + i.Uint64 = n + i.Valid = true +} + +// Ptr returns a pointer to this Uint64's value, or a nil pointer if this Uint64 is null. +func (i Uint64) Ptr() *uint64 { + if !i.Valid { + return nil + } + return &i.Uint64 +} + +// IsZero returns true for invalid Uint64's, for future omitempty support (Go 1.4?) +// A non-null Uint64 with a 0 value will not be considered zero. +func (i Uint64) IsZero() bool { + return !i.Valid +} + +// Scan implements the Scanner interface. +func (n *NullUint64) Scan(value interface{}) error { + if value == nil { + n.Uint64, n.Valid = 0, false + return nil + } + n.Valid = true + return convertAssign(&n.Uint64, value) +} + +// Value implements the driver Valuer interface. +func (n NullUint64) Value() (driver.Value, error) { + if !n.Valid { + return nil, nil + } + return n.Uint64, nil +} diff --git a/uint64_test.go b/uint64_test.go new file mode 100644 index 0000000..8d6ede0 --- /dev/null +++ b/uint64_test.go @@ -0,0 +1,178 @@ +package null + +import ( + "encoding/json" + "testing" +) + +var ( + uint64JSON = []byte(`18446744073709551614`) + nullUint64JSON = []byte(`{"Uint64":18446744073709551614,"Valid":true}`) +) + +func TestUint64From(t *testing.T) { + i := Uint64From(18446744073709551614) + assertUint64(t, i, "Uint64From()") + + zero := Uint64From(0) + if !zero.Valid { + t.Error("Uint64From(0)", "is invalid, but should be valid") + } +} + +func TestUint64FromPtr(t *testing.T) { + n := uint64(18446744073709551614) + iptr := &n + i := Uint64FromPtr(iptr) + assertUint64(t, i, "Uint64FromPtr()") + + null := Uint64FromPtr(nil) + assertNullUint64(t, null, "Uint64FromPtr(nil)") +} + +func TestUnmarshalUint64(t *testing.T) { + var i Uint64 + err := json.Unmarshal(uint64JSON, &i) + maybePanic(err) + assertUint64(t, i, "uint64 json") + + var ni Uint64 + err = json.Unmarshal(nullUint64JSON, &ni) + maybePanic(err) + assertUint64(t, ni, "sq.NullUint64 json") + + var null Uint64 + err = json.Unmarshal(nullJSON, &null) + maybePanic(err) + assertNullUint64(t, null, "null json") + + var badType Uint64 + err = json.Unmarshal(boolJSON, &badType) + if err == nil { + panic("err should not be nil") + } + assertNullUint64(t, badType, "wrong type json") + + var invalid Uint64 + err = invalid.UnmarshalJSON(invalidJSON) + if _, ok := err.(*json.SyntaxError); !ok { + t.Errorf("expected json.SyntaxError, not %T", err) + } + assertNullUint64(t, invalid, "invalid json") +} + +func TestUnmarshalNonUintegerNumber64(t *testing.T) { + var i Uint64 + err := json.Unmarshal(float64JSON, &i) + if err == nil { + panic("err should be present; non-integer number coerced to uint64") + } +} + +func TestTextUnmarshalUint64(t *testing.T) { + var i Uint64 + err := i.UnmarshalText([]byte("18446744073709551614")) + maybePanic(err) + assertUint64(t, i, "UnmarshalText() uint64") + + var blank Uint64 + err = blank.UnmarshalText([]byte("")) + maybePanic(err) + assertNullUint64(t, blank, "UnmarshalText() empty uint64") + + var null Uint64 + err = null.UnmarshalText([]byte("null")) + maybePanic(err) + assertNullUint64(t, null, `UnmarshalText() "null"`) +} + +func TestMarshalUint64(t *testing.T) { + i := Uint64From(18446744073709551614) + data, err := json.Marshal(i) + maybePanic(err) + assertJSONEquals(t, data, "18446744073709551614", "non-empty json marshal") + + // invalid values should be encoded as null + null := NewUint64(0, false) + data, err = json.Marshal(null) + maybePanic(err) + assertJSONEquals(t, data, "null", "null json marshal") +} + +func TestMarshalUint64Text(t *testing.T) { + i := Uint64From(18446744073709551614) + data, err := i.MarshalText() + maybePanic(err) + assertJSONEquals(t, data, "18446744073709551614", "non-empty text marshal") + + // invalid values should be encoded as null + null := NewUint64(0, false) + data, err = null.MarshalText() + maybePanic(err) + assertJSONEquals(t, data, "", "null text marshal") +} + +func TestUint64Pointer(t *testing.T) { + i := Uint64From(18446744073709551614) + ptr := i.Ptr() + if *ptr != 18446744073709551614 { + t.Errorf("bad %s uint64: %#v ≠ %d\n", "pointer", ptr, uint64(18446744073709551614)) + } + + null := NewUint64(0, false) + ptr = null.Ptr() + if ptr != nil { + t.Errorf("bad %s uint64: %#v ≠ %s\n", "nil pointer", ptr, "nil") + } +} + +func TestUint64IsZero(t *testing.T) { + i := Uint64From(18446744073709551614) + if i.IsZero() { + t.Errorf("IsZero() should be false") + } + + null := NewUint64(0, false) + if !null.IsZero() { + t.Errorf("IsZero() should be true") + } + + zero := NewUint64(0, true) + if zero.IsZero() { + t.Errorf("IsZero() should be false") + } +} + +func TestUint64SetValid(t *testing.T) { + change := NewUint64(0, false) + assertNullUint64(t, change, "SetValid()") + change.SetValid(18446744073709551614) + assertUint64(t, change, "SetValid()") +} + +func TestUint64Scan(t *testing.T) { + var i Uint64 + err := i.Scan(uint64(18446744073709551614)) + maybePanic(err) + assertUint64(t, i, "scanned uint64") + + var null Uint64 + err = null.Scan(nil) + maybePanic(err) + assertNullUint64(t, null, "scanned null") +} + +func assertUint64(t *testing.T, i Uint64, from string) { + if i.Uint64 != 18446744073709551614 { + t.Errorf("bad %s uint64: %d ≠ %d\n", from, i.Uint64, uint64(18446744073709551614)) + } + if !i.Valid { + t.Error(from, "is invalid, but should be valid") + } +} + +func assertNullUint64(t *testing.T, i Uint64, from string) { + if i.Valid { + t.Error(from, "is valid, but should be invalid") + } +} diff --git a/uint8.go b/uint8.go new file mode 100644 index 0000000..16d4421 --- /dev/null +++ b/uint8.go @@ -0,0 +1,145 @@ +package null + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "reflect" + "strconv" +) + +// NullUint8 is a replica of sql.NullInt64 for uint8 types. +type NullUint8 struct { + Uint8 uint8 + Valid bool +} + +// Uint8 is an nullable uint8. +// It does not consider zero values to be null. +// It will decode to null, not zero, if null. +type Uint8 struct { + NullUint8 +} + +// NewUint8 creates a new Uint8 +func NewUint8(i uint8, valid bool) Uint8 { + return Uint8{ + NullUint8: NullUint8{ + Uint8: i, + Valid: valid, + }, + } +} + +// Uint8From creates a new Uint8 that will always be valid. +func Uint8From(i uint8) Uint8 { + return NewUint8(i, true) +} + +// Uint8FromPtr creates a new Uint8 that be null if i is nil. +func Uint8FromPtr(i *uint8) Uint8 { + if i == nil { + return NewUint8(0, false) + } + return NewUint8(*i, true) +} + +// UnmarshalJSON implements json.Unmarshaler. +// It supports number and null input. +// 0 will not be considered a null Uint8. +// It also supports unmarshalling a sql.NullUint8. +func (i *Uint8) UnmarshalJSON(data []byte) error { + var err error + var v interface{} + if err = json.Unmarshal(data, &v); err != nil { + return err + } + switch v.(type) { + case float64: + // Unmarshal again, directly to uint8, to avoid intermediate float64 + err = json.Unmarshal(data, &i.Uint8) + case map[string]interface{}: + err = json.Unmarshal(data, &i.NullUint8) + case nil: + i.Valid = false + return nil + default: + err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Uint8", reflect.TypeOf(v).Name()) + } + i.Valid = err == nil + return err +} + +// UnmarshalText implements encoding.TextUnmarshaler. +// It will unmarshal to a null Uint8 if the input is a blank or not an integer. +// It will return an error if the input is not an integer, blank, or "null". +func (i *Uint8) UnmarshalText(text []byte) error { + str := string(text) + if str == "" || str == "null" { + i.Valid = false + return nil + } + var err error + res, err := strconv.ParseUint(string(text), 10, 8) + i.Valid = err == nil + if i.Valid { + i.Uint8 = uint8(res) + } + return err +} + +// MarshalJSON implements json.Marshaler. +// It will encode null if this Uint8 is null. +func (i Uint8) MarshalJSON() ([]byte, error) { + if !i.Valid { + return []byte("null"), nil + } + return []byte(strconv.FormatUint(uint64(i.Uint8), 10)), nil +} + +// MarshalText implements encoding.TextMarshaler. +// It will encode a blank string if this Uint8 is null. +func (i Uint8) MarshalText() ([]byte, error) { + if !i.Valid { + return []byte{}, nil + } + return []byte(strconv.FormatUint(uint64(i.Uint8), 10)), nil +} + +// SetValid changes this Uint8's value and also sets it to be non-null. +func (i *Uint8) SetValid(n uint8) { + i.Uint8 = n + i.Valid = true +} + +// Ptr returns a pointer to this Uint8's value, or a nil pointer if this Uint8 is null. +func (i Uint8) Ptr() *uint8 { + if !i.Valid { + return nil + } + return &i.Uint8 +} + +// IsZero returns true for invalid Uint8's, for future omitempty support (Go 1.4?) +// A non-null Uint8 with a 0 value will not be considered zero. +func (i Uint8) IsZero() bool { + return !i.Valid +} + +// Scan implements the Scanner interface. +func (n *NullUint8) Scan(value interface{}) error { + if value == nil { + n.Uint8, n.Valid = 0, false + return nil + } + n.Valid = true + return convertAssign(&n.Uint8, value) +} + +// Value implements the driver Valuer interface. +func (n NullUint8) Value() (driver.Value, error) { + if !n.Valid { + return nil, nil + } + return n.Uint8, nil +} diff --git a/uint8_test.go b/uint8_test.go new file mode 100644 index 0000000..a59b08a --- /dev/null +++ b/uint8_test.go @@ -0,0 +1,196 @@ +package null + +import ( + "encoding/json" + "math" + "strconv" + "testing" +) + +var ( + uint8JSON = []byte(`254`) + nullUint8JSON = []byte(`{"Uint8":254,"Valid":true}`) +) + +func TestUint8From(t *testing.T) { + i := Uint8From(254) + assertUint8(t, i, "Uint8From()") + + zero := Uint8From(0) + if !zero.Valid { + t.Error("Uint8From(0)", "is invalid, but should be valid") + } +} + +func TestUint8FromPtr(t *testing.T) { + n := uint8(254) + iptr := &n + i := Uint8FromPtr(iptr) + assertUint8(t, i, "Uint8FromPtr()") + + null := Uint8FromPtr(nil) + assertNullUint8(t, null, "Uint8FromPtr(nil)") +} + +func TestUnmarshalUint8(t *testing.T) { + var i Uint8 + err := json.Unmarshal(uint8JSON, &i) + maybePanic(err) + assertUint8(t, i, "uint8 json") + + var ni Uint8 + err = json.Unmarshal(nullUint8JSON, &ni) + maybePanic(err) + assertUint8(t, ni, "sq.NullUint8 json") + + var null Uint8 + err = json.Unmarshal(nullJSON, &null) + maybePanic(err) + assertNullUint8(t, null, "null json") + + var badType Uint8 + err = json.Unmarshal(boolJSON, &badType) + if err == nil { + panic("err should not be nil") + } + assertNullUint8(t, badType, "wrong type json") + + var invalid Uint8 + err = invalid.UnmarshalJSON(invalidJSON) + if _, ok := err.(*json.SyntaxError); !ok { + t.Errorf("expected json.SyntaxError, not %T", err) + } + assertNullUint8(t, invalid, "invalid json") +} + +func TestUnmarshalNonUintegerNumber8(t *testing.T) { + var i Uint8 + err := json.Unmarshal(float64JSON, &i) + if err == nil { + panic("err should be present; non-integer number coerced to uint8") + } +} + +func TestUnmarshalUint8Overflow(t *testing.T) { + uint8Overflow := int64(math.MaxUint8) + + // Max uint8 should decode successfully + var i Uint8 + err := json.Unmarshal([]byte(strconv.FormatUint(uint64(uint8Overflow), 10)), &i) + maybePanic(err) + + // Attempt to overflow + uint8Overflow++ + err = json.Unmarshal([]byte(strconv.FormatUint(uint64(uint8Overflow), 10)), &i) + if err == nil { + panic("err should be present; decoded value overflows uint8") + } +} + +func TestTextUnmarshalUint8(t *testing.T) { + var i Uint8 + err := i.UnmarshalText([]byte("254")) + maybePanic(err) + assertUint8(t, i, "UnmarshalText() uint8") + + var blank Uint8 + err = blank.UnmarshalText([]byte("")) + maybePanic(err) + assertNullUint8(t, blank, "UnmarshalText() empty uint8") + + var null Uint8 + err = null.UnmarshalText([]byte("null")) + maybePanic(err) + assertNullUint8(t, null, `UnmarshalText() "null"`) +} + +func TestMarshalUint8(t *testing.T) { + i := Uint8From(254) + data, err := json.Marshal(i) + maybePanic(err) + assertJSONEquals(t, data, "254", "non-empty json marshal") + + // invalid values should be encoded as null + null := NewUint8(0, false) + data, err = json.Marshal(null) + maybePanic(err) + assertJSONEquals(t, data, "null", "null json marshal") +} + +func TestMarshalUint8Text(t *testing.T) { + i := Uint8From(254) + data, err := i.MarshalText() + maybePanic(err) + assertJSONEquals(t, data, "254", "non-empty text marshal") + + // invalid values should be encoded as null + null := NewUint8(0, false) + data, err = null.MarshalText() + maybePanic(err) + assertJSONEquals(t, data, "", "null text marshal") +} + +func TestUint8Pointer(t *testing.T) { + i := Uint8From(254) + ptr := i.Ptr() + if *ptr != 254 { + t.Errorf("bad %s uint8: %#v ≠ %d\n", "pointer", ptr, 254) + } + + null := NewUint8(0, false) + ptr = null.Ptr() + if ptr != nil { + t.Errorf("bad %s uint8: %#v ≠ %s\n", "nil pointer", ptr, "nil") + } +} + +func TestUint8IsZero(t *testing.T) { + i := Uint8From(254) + if i.IsZero() { + t.Errorf("IsZero() should be false") + } + + null := NewUint8(0, false) + if !null.IsZero() { + t.Errorf("IsZero() should be true") + } + + zero := NewUint8(0, true) + if zero.IsZero() { + t.Errorf("IsZero() should be false") + } +} + +func TestUint8SetValid(t *testing.T) { + change := NewUint8(0, false) + assertNullUint8(t, change, "SetValid()") + change.SetValid(254) + assertUint8(t, change, "SetValid()") +} + +func TestUint8Scan(t *testing.T) { + var i Uint8 + err := i.Scan(254) + maybePanic(err) + assertUint8(t, i, "scanned uint8") + + var null Uint8 + err = null.Scan(nil) + maybePanic(err) + assertNullUint8(t, null, "scanned null") +} + +func assertUint8(t *testing.T, i Uint8, from string) { + if i.Uint8 != 254 { + t.Errorf("bad %s uint8: %d ≠ %d\n", from, i.Uint8, 254) + } + if !i.Valid { + t.Error(from, "is invalid, but should be valid") + } +} + +func assertNullUint8(t *testing.T, i Uint8, from string) { + if i.Valid { + t.Error(from, "is valid, but should be invalid") + } +} diff --git a/uint_test.go b/uint_test.go new file mode 100644 index 0000000..42db01a --- /dev/null +++ b/uint_test.go @@ -0,0 +1,178 @@ +package null + +import ( + "encoding/json" + "testing" +) + +var ( + uintJSON = []byte(`12345`) + nullUintJSON = []byte(`{"Uint":12345,"Valid":true}`) +) + +func TestUintFrom(t *testing.T) { + i := UintFrom(12345) + assertUint(t, i, "UintFrom()") + + zero := UintFrom(0) + if !zero.Valid { + t.Error("UintFrom(0)", "is invalid, but should be valid") + } +} + +func TestUintFromPtr(t *testing.T) { + n := uint(12345) + iptr := &n + i := UintFromPtr(iptr) + assertUint(t, i, "UintFromPtr()") + + null := UintFromPtr(nil) + assertNullUint(t, null, "UintFromPtr(nil)") +} + +func TestUnmarshalUint(t *testing.T) { + var i Uint + err := json.Unmarshal(uintJSON, &i) + maybePanic(err) + assertUint(t, i, "uint json") + + var ni Uint + err = json.Unmarshal(nullUintJSON, &ni) + maybePanic(err) + assertUint(t, ni, "sq.NullUint json") + + var null Uint + err = json.Unmarshal(nullJSON, &null) + maybePanic(err) + assertNullUint(t, null, "null json") + + var badType Uint + err = json.Unmarshal(boolJSON, &badType) + if err == nil { + panic("err should not be nil") + } + assertNullUint(t, badType, "wrong type json") + + var invalid Uint + err = invalid.UnmarshalJSON(invalidJSON) + if _, ok := err.(*json.SyntaxError); !ok { + t.Errorf("expected json.SyntaxError, not %T", err) + } + assertNullUint(t, invalid, "invalid json") +} + +func TestUnmarshalNonUintegerNumber(t *testing.T) { + var i Uint + err := json.Unmarshal(float64JSON, &i) + if err == nil { + panic("err should be present; non-uinteger number coerced to uint") + } +} + +func TestTextUnmarshalUint(t *testing.T) { + var i Uint + err := i.UnmarshalText([]byte("12345")) + maybePanic(err) + assertUint(t, i, "UnmarshalText() uint") + + var blank Uint + err = blank.UnmarshalText([]byte("")) + maybePanic(err) + assertNullUint(t, blank, "UnmarshalText() empty uint") + + var null Uint + err = null.UnmarshalText([]byte("null")) + maybePanic(err) + assertNullUint(t, null, `UnmarshalText() "null"`) +} + +func TestMarshalUint(t *testing.T) { + i := UintFrom(12345) + data, err := json.Marshal(i) + maybePanic(err) + assertJSONEquals(t, data, "12345", "non-empty json marshal") + + // invalid values should be encoded as null + null := NewUint(0, false) + data, err = json.Marshal(null) + maybePanic(err) + assertJSONEquals(t, data, "null", "null json marshal") +} + +func TestMarshalUintText(t *testing.T) { + i := UintFrom(12345) + data, err := i.MarshalText() + maybePanic(err) + assertJSONEquals(t, data, "12345", "non-empty text marshal") + + // invalid values should be encoded as null + null := NewUint(0, false) + data, err = null.MarshalText() + maybePanic(err) + assertJSONEquals(t, data, "", "null text marshal") +} + +func TestUintPointer(t *testing.T) { + i := UintFrom(12345) + ptr := i.Ptr() + if *ptr != 12345 { + t.Errorf("bad %s uint: %#v ≠ %d\n", "pointer", ptr, 12345) + } + + null := NewUint(0, false) + ptr = null.Ptr() + if ptr != nil { + t.Errorf("bad %s uint: %#v ≠ %s\n", "nil pointer", ptr, "nil") + } +} + +func TestUintIsZero(t *testing.T) { + i := UintFrom(12345) + if i.IsZero() { + t.Errorf("IsZero() should be false") + } + + null := NewUint(0, false) + if !null.IsZero() { + t.Errorf("IsZero() should be true") + } + + zero := NewUint(0, true) + if zero.IsZero() { + t.Errorf("IsZero() should be false") + } +} + +func TestUintSetValid(t *testing.T) { + change := NewUint(0, false) + assertNullUint(t, change, "SetValid()") + change.SetValid(12345) + assertUint(t, change, "SetValid()") +} + +func TestUintScan(t *testing.T) { + var i Uint + err := i.Scan(12345) + maybePanic(err) + assertUint(t, i, "scanned uint") + + var null Uint + err = null.Scan(nil) + maybePanic(err) + assertNullUint(t, null, "scanned null") +} + +func assertUint(t *testing.T, i Uint, from string) { + if i.Uint != 12345 { + t.Errorf("bad %s uint: %d ≠ %d\n", from, i.Uint, 12345) + } + if !i.Valid { + t.Error(from, "is invalid, but should be valid") + } +} + +func assertNullUint(t *testing.T, i Uint, from string) { + if i.Valid { + t.Error(from, "is valid, but should be invalid") + } +}