├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── bool.go
├── bool_test.go
├── doc.go
├── float64.go
├── float64_test.go
├── go.mod
├── int32.go
├── int32_test.go
├── int64.go
├── int64_test.go
├── nullgopher.svg
├── string.go
├── string_test.go
├── time.go
└── time_test.go
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contribute
2 |
3 | Report bugs on GitHub and discuss your ideas and feature requests before you open a pull request. Pull requests must pass the test suite and add new tests for each new feature. Bugs should be validated by tests. The Go code must be formatted by gofmt.
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Emvi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Nullable Go types
2 |
3 | [](https://pkg.go.dev/github.com/emvi/null?status)
4 | [](https://circleci.com/gh/emvi/null)
5 | [](https://goreportcard.com/report/github.com/emvi/null)
6 |
7 |
8 | ## Description
9 |
10 | This package provides nullable Go types for bool, float64, int64, int32, string and time.Time replacing sql.NullString, sql.NullInt64, ... that can be marshalled/unmarshalled to/from JSON.
11 |
12 | ## Installation
13 |
14 | To install "null", run go get within your project:
15 |
16 | ```
17 | go get github.com/emvi/null
18 | ```
19 |
20 | Note that from 1.3 on "null" requires Go version 1.13 or newer.
21 |
22 | ## Usage
23 |
24 | Here is a short example demonstrating the string type. The other types (int64, float64 and bool) work in the same manner.
25 |
26 | ```
27 | package main
28 |
29 | import (
30 | "encoding/json"
31 | "database/sql"
32 | "fmt"
33 |
34 | "github.com/emvi/null"
35 | )
36 |
37 | type NullableString struct {
38 | Value null.String `json:"value"`
39 | }
40 |
41 | func main() {
42 | str := NullableString{null.NewString("nullable string", true)}
43 | // or long version: str := NullableString{null.String{sql.NullString{String: "nullable string", Valid: true}}}
44 |
45 | data, _ := json.Marshal(str)
46 | fmt.Println(string(data)) // -> {"value": "nullable"}
47 |
48 | str.SetNil() // use str.SetValid("value") to set a value again
49 | data, _ = json.Marshal(str)
50 | fmt.Println(string(data)) // -> {"value": null}
51 | }
52 | ```
53 |
54 | ## Contribute
55 |
56 | [See CONTRIBUTING.md](CONTRIBUTING.md)
57 |
58 | ## License
59 |
60 | MIT
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/bool.go:
--------------------------------------------------------------------------------
1 | package null
2 |
3 | import (
4 | "database/sql"
5 | "encoding/json"
6 | )
7 |
8 | // Bool is a nullable boolean type based on sql.NullBool, that supports parsing to/from JSON.
9 | type Bool struct {
10 | sql.NullBool
11 | }
12 |
13 | // NewBool returns a new nullable Bool object.
14 | // This is equivalent to `null.Bool{sql.NullBool{Bool: b, Valid: valid}}`.
15 | func NewBool(b, valid bool) Bool {
16 | return Bool{sql.NullBool{Bool: b, Valid: valid}}
17 | }
18 |
19 | // MarshalJSON implements the encoding json interface.
20 | func (b Bool) MarshalJSON() ([]byte, error) {
21 | if b.Valid {
22 | return json.Marshal(b.Bool)
23 | }
24 |
25 | return json.Marshal(nil)
26 | }
27 |
28 | // UnmarshalJSON implements the encoding json interface.
29 | func (b *Bool) UnmarshalJSON(data []byte) error {
30 | var value *bool
31 |
32 | if err := json.Unmarshal(data, &value); err != nil {
33 | return err
34 | }
35 |
36 | if value != nil {
37 | b.SetValid(*value)
38 | } else {
39 | b.SetNil()
40 | }
41 |
42 | return nil
43 | }
44 |
45 | // SetValid sets the value and valid to true.
46 | func (b *Bool) SetValid(value bool) {
47 | b.Bool = value
48 | b.Valid = true
49 | }
50 |
51 | // SetNil sets the value to default and valid to false.
52 | func (b *Bool) SetNil() {
53 | b.Bool = false
54 | b.Valid = false
55 | }
56 |
--------------------------------------------------------------------------------
/bool_test.go:
--------------------------------------------------------------------------------
1 | package null
2 |
3 | import (
4 | "database/sql"
5 | "encoding/json"
6 | "testing"
7 | )
8 |
9 | type testBool struct {
10 | Value Bool `json:"value"`
11 | }
12 |
13 | func TestNewBool(t *testing.T) {
14 | value := NewBool(true, true)
15 |
16 | if !value.Bool || !value.Valid {
17 | t.Fatal("New Bool must have value and be valid")
18 | }
19 | }
20 |
21 | func TestMarshalBool(t *testing.T) {
22 | value := Bool{sql.NullBool{Bool: true, Valid: true}}
23 |
24 | if data, err := json.Marshal(value); err != nil || string(data) != "true" {
25 | t.Fatalf("Bool must be marshalled to value, but was %v %v", err, string(data))
26 | }
27 |
28 | value.Valid = false
29 |
30 | if data, err := json.Marshal(value); err != nil || string(data) != "null" {
31 | t.Fatalf("Bool must be marshalled to null, but was %v %v", err, string(data))
32 | }
33 | }
34 |
35 | func TestUnmarshalBool(t *testing.T) {
36 | str := `{"value": true}`
37 | var value testBool
38 |
39 | if err := json.Unmarshal([]byte(str), &value); err != nil {
40 | t.Fatalf("Bool must be unmarshalled to value, but was %v", err)
41 | }
42 |
43 | if !value.Value.Valid || !value.Value.Bool {
44 | t.Fatalf("Unmarshalled null bool must be valid, but was %v", value.Value)
45 | }
46 |
47 | str = `{"value": null}`
48 |
49 | if err := json.Unmarshal([]byte(str), &value); err != nil {
50 | t.Fatalf("Bool must be unmarshalled to null, but was %v", err)
51 | }
52 |
53 | if value.Value.Valid {
54 | t.Fatal("Unmarshalled null bool must be invalid")
55 | }
56 | }
57 |
58 | func TestGettersSettersBool(t *testing.T) {
59 | value := NewBool(true, true)
60 | value.SetNil()
61 |
62 | if value.Bool || value.Valid {
63 | t.Fatal("Bool must be nil")
64 | }
65 |
66 | value.SetValid(true)
67 |
68 | if !value.Bool || !value.Valid {
69 | t.Fatal("Bool must be valid")
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/doc.go:
--------------------------------------------------------------------------------
1 | // Package null provides nullable types (based on database/sql) that can be parsed to and from JSON.
2 | package null
3 |
--------------------------------------------------------------------------------
/float64.go:
--------------------------------------------------------------------------------
1 | package null
2 |
3 | import (
4 | "database/sql"
5 | "encoding/json"
6 | )
7 |
8 | // Float64 is a nullable float64 type based on sql.NullFloat64, that supports parsing to/from JSON.
9 | type Float64 struct {
10 | sql.NullFloat64
11 | }
12 |
13 | // NewFloat64 returns a new nullable Float64 object.
14 | // This is equivalent to `null.Float64{sql.NullFloat64{Float64: f, Valid: valid}}`.
15 | func NewFloat64(f float64, valid bool) Float64 {
16 | return Float64{sql.NullFloat64{Float64: f, Valid: valid}}
17 | }
18 |
19 | // MarshalJSON implements the encoding json interface.
20 | func (f Float64) MarshalJSON() ([]byte, error) {
21 | if f.Valid {
22 | return json.Marshal(f.Float64)
23 | }
24 |
25 | return json.Marshal(nil)
26 | }
27 |
28 | // UnmarshalJSON implements the encoding json interface.
29 | func (f *Float64) UnmarshalJSON(data []byte) error {
30 | var value *float64
31 |
32 | if err := json.Unmarshal(data, &value); err != nil {
33 | return err
34 | }
35 |
36 | if value != nil {
37 | f.SetValid(*value)
38 | } else {
39 | f.SetNil()
40 | }
41 |
42 | return nil
43 | }
44 |
45 | // SetValid sets the value and valid to true.
46 | func (f *Float64) SetValid(value float64) {
47 | f.Float64 = value
48 | f.Valid = true
49 | }
50 |
51 | // SetNil sets the value to default and valid to false.
52 | func (f *Float64) SetNil() {
53 | f.Float64 = 0
54 | f.Valid = false
55 | }
56 |
--------------------------------------------------------------------------------
/float64_test.go:
--------------------------------------------------------------------------------
1 | package null
2 |
3 | import (
4 | "database/sql"
5 | "encoding/json"
6 | "testing"
7 | )
8 |
9 | type testFloat64 struct {
10 | Value Float64 `json:"value"`
11 | }
12 |
13 | func TestNewFloat64(t *testing.T) {
14 | value := NewFloat64(123.45, true)
15 |
16 | if value.Float64 != 123.45 || !value.Valid {
17 | t.Fatal("New Float64 must have value and be valid")
18 | }
19 | }
20 |
21 | func TestMarshalFloat64(t *testing.T) {
22 | value := Float64{sql.NullFloat64{Float64: 123.45, Valid: true}}
23 |
24 | if data, err := json.Marshal(value); err != nil || string(data) != "123.45" {
25 | t.Fatalf("Float64 must be marshalled to value, but was %v %v", err, string(data))
26 | }
27 |
28 | value.Valid = false
29 |
30 | if data, err := json.Marshal(value); err != nil || string(data) != "null" {
31 | t.Fatalf("Float64 must be marshalled to null, but was %v %v", err, string(data))
32 | }
33 | }
34 |
35 | func TestUnmarshalFloat64(t *testing.T) {
36 | str := `{"value": 123.45}`
37 | var value testFloat64
38 |
39 | if err := json.Unmarshal([]byte(str), &value); err != nil {
40 | t.Fatalf("Float64 must be unmarshalled to value, but was %v", err)
41 | }
42 |
43 | if !value.Value.Valid || value.Value.Float64 != 123.45 {
44 | t.Fatalf("Unmarshalled null float64 must be valid, but was %v", value.Value)
45 | }
46 |
47 | str = `{"value": null}`
48 |
49 | if err := json.Unmarshal([]byte(str), &value); err != nil {
50 | t.Fatalf("String must be unmarshalled to null, but was %v", err)
51 | }
52 |
53 | if value.Value.Valid {
54 | t.Fatal("Unmarshalled null float64 must be invalid")
55 | }
56 | }
57 |
58 | func TestGettersSettersFloat64(t *testing.T) {
59 | value := NewFloat64(123.45, true)
60 | value.SetNil()
61 |
62 | if value.Float64 != 0 || value.Valid {
63 | t.Fatal("Float64 must be nil")
64 | }
65 |
66 | value.SetValid(123.45)
67 |
68 | if value.Float64 != 123.45 || !value.Valid {
69 | t.Fatal("Float64 must be valid")
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/emvi/null
2 |
3 | go 1.17
4 |
--------------------------------------------------------------------------------
/int32.go:
--------------------------------------------------------------------------------
1 | package null
2 |
3 | import (
4 | "database/sql"
5 | "encoding/json"
6 | )
7 |
8 | // Int32 is a nullable int32 type based on sql.NullInt32, that supports parsing to/from JSON.
9 | type Int32 struct {
10 | sql.NullInt32
11 | }
12 |
13 | // NewInt32 returns a new nullable Int32 object.
14 | // This is equivalent to `null.Int32{sql.NullInt32{Int32: i, Valid: valid}}`.
15 | func NewInt32(i int32, valid bool) Int32 {
16 | return Int32{sql.NullInt32{Int32: i, Valid: valid}}
17 | }
18 |
19 | // MarshalJSON implements the encoding json interface.
20 | func (i Int32) MarshalJSON() ([]byte, error) {
21 | if i.Valid {
22 | return json.Marshal(i.Int32)
23 | }
24 |
25 | return json.Marshal(nil)
26 | }
27 |
28 | // UnmarshalJSON implements the encoding json interface.
29 | func (i *Int32) UnmarshalJSON(data []byte) error {
30 | var value *int32
31 |
32 | if err := json.Unmarshal(data, &value); err != nil {
33 | return err
34 | }
35 |
36 | if value != nil {
37 | i.SetValid(*value)
38 | } else {
39 | i.SetNil()
40 | }
41 |
42 | return nil
43 | }
44 |
45 | // SetValid sets the value and valid to true.
46 | func (i *Int32) SetValid(value int32) {
47 | i.Int32 = value
48 | i.Valid = true
49 | }
50 |
51 | // SetNil sets the value to default and valid to false.
52 | func (i *Int32) SetNil() {
53 | i.Int32 = 0
54 | i.Valid = false
55 | }
56 |
--------------------------------------------------------------------------------
/int32_test.go:
--------------------------------------------------------------------------------
1 | package null
2 |
3 | import (
4 | "database/sql"
5 | "encoding/json"
6 | "testing"
7 | )
8 |
9 | type testInt32 struct {
10 | Value Int32 `json:"value"`
11 | }
12 |
13 | func TestNewInt32(t *testing.T) {
14 | value := NewInt32(123, true)
15 |
16 | if value.Int32 != 123 || !value.Valid {
17 | t.Fatal("New Int32 must have value and be valid")
18 | }
19 | }
20 |
21 | func TestMarshalInt32(t *testing.T) {
22 | value := Int32{sql.NullInt32{Int32: 123, Valid: true}}
23 |
24 | if data, err := json.Marshal(value); err != nil || string(data) != "123" {
25 | t.Fatalf("Int32 must be marshalled to value, but was %v %v", err, string(data))
26 | }
27 |
28 | value.Valid = false
29 |
30 | if data, err := json.Marshal(value); err != nil || string(data) != "null" {
31 | t.Fatalf("Int32 must be marshalled to null, but was %v %v", err, string(data))
32 | }
33 | }
34 |
35 | func TestUnmarshalInt32(t *testing.T) {
36 | str := `{"value": 123}`
37 | var value testInt32
38 |
39 | if err := json.Unmarshal([]byte(str), &value); err != nil {
40 | t.Fatalf("Int32 must be unmarshalled to value, but was %v", err)
41 | }
42 |
43 | if !value.Value.Valid || value.Value.Int32 != 123 {
44 | t.Fatalf("Unmarshalled null Int32 must be valid, but was %v", value.Value)
45 | }
46 |
47 | str = `{"value": null}`
48 |
49 | if err := json.Unmarshal([]byte(str), &value); err != nil {
50 | t.Fatalf("Int32 must be unmarshalled to null, but was %v", err)
51 | }
52 |
53 | if value.Value.Valid {
54 | t.Fatal("Unmarshalled null Int32 must be invalid")
55 | }
56 | }
57 |
58 | func TestGettersSettersInt32(t *testing.T) {
59 | value := NewInt32(123, true)
60 | value.SetNil()
61 |
62 | if value.Int32 != 0 || value.Valid {
63 | t.Fatal("Int32 must be nil")
64 | }
65 |
66 | value.SetValid(123)
67 |
68 | if value.Int32 != 123 || !value.Valid {
69 | t.Fatal("Int32 must be valid")
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/int64.go:
--------------------------------------------------------------------------------
1 | package null
2 |
3 | import (
4 | "database/sql"
5 | "encoding/json"
6 | )
7 |
8 | // Int64 is a nullable int64 type based on sql.NullInt64, that supports parsing to/from JSON.
9 | type Int64 struct {
10 | sql.NullInt64
11 | }
12 |
13 | // NewInt64 returns a new nullable Int64 object.
14 | // This is equivalent to `null.Int64{sql.NullInt64{Int64: i, Valid: valid}}`.
15 | func NewInt64(i int64, valid bool) Int64 {
16 | return Int64{sql.NullInt64{Int64: i, Valid: valid}}
17 | }
18 |
19 | // MarshalJSON implements the encoding json interface.
20 | func (i Int64) MarshalJSON() ([]byte, error) {
21 | if i.Valid {
22 | return json.Marshal(i.Int64)
23 | }
24 |
25 | return json.Marshal(nil)
26 | }
27 |
28 | // UnmarshalJSON implements the encoding json interface.
29 | func (i *Int64) UnmarshalJSON(data []byte) error {
30 | var value *int64
31 |
32 | if err := json.Unmarshal(data, &value); err != nil {
33 | return err
34 | }
35 |
36 | if value != nil {
37 | i.SetValid(*value)
38 | } else {
39 | i.SetNil()
40 | }
41 |
42 | return nil
43 | }
44 |
45 | // SetValid sets the value and valid to true.
46 | func (i *Int64) SetValid(value int64) {
47 | i.Int64 = value
48 | i.Valid = true
49 | }
50 |
51 | // SetNil sets the value to default and valid to false.
52 | func (i *Int64) SetNil() {
53 | i.Int64 = 0
54 | i.Valid = false
55 | }
56 |
--------------------------------------------------------------------------------
/int64_test.go:
--------------------------------------------------------------------------------
1 | package null
2 |
3 | import (
4 | "database/sql"
5 | "encoding/json"
6 | "testing"
7 | )
8 |
9 | type testInt64 struct {
10 | Value Int64 `json:"value"`
11 | }
12 |
13 | func TestNewInt64(t *testing.T) {
14 | value := NewInt64(123, true)
15 |
16 | if value.Int64 != 123 || !value.Valid {
17 | t.Fatal("New Int64 must have value and be valid")
18 | }
19 | }
20 |
21 | func TestMarshalInt64(t *testing.T) {
22 | value := Int64{sql.NullInt64{Int64: 123, Valid: true}}
23 |
24 | if data, err := json.Marshal(value); err != nil || string(data) != "123" {
25 | t.Fatalf("Int64 must be marshalled to value, but was %v %v", err, string(data))
26 | }
27 |
28 | value.Valid = false
29 |
30 | if data, err := json.Marshal(value); err != nil || string(data) != "null" {
31 | t.Fatalf("Int64 must be marshalled to null, but was %v %v", err, string(data))
32 | }
33 | }
34 |
35 | func TestUnmarshalInt64(t *testing.T) {
36 | str := `{"value": 123}`
37 | var value testInt64
38 |
39 | if err := json.Unmarshal([]byte(str), &value); err != nil {
40 | t.Fatalf("Int64 must be unmarshalled to value, but was %v", err)
41 | }
42 |
43 | if !value.Value.Valid || value.Value.Int64 != 123 {
44 | t.Fatalf("Unmarshalled null int64 must be valid, but was %v", value.Value)
45 | }
46 |
47 | str = `{"value": null}`
48 |
49 | if err := json.Unmarshal([]byte(str), &value); err != nil {
50 | t.Fatalf("Int64 must be unmarshalled to null, but was %v", err)
51 | }
52 |
53 | if value.Value.Valid {
54 | t.Fatal("Unmarshalled null int64 must be invalid")
55 | }
56 | }
57 |
58 | func TestGettersSettersInt64(t *testing.T) {
59 | value := NewInt64(123, true)
60 | value.SetNil()
61 |
62 | if value.Int64 != 0 || value.Valid {
63 | t.Fatal("Int64 must be nil")
64 | }
65 |
66 | value.SetValid(123)
67 |
68 | if value.Int64 != 123 || !value.Valid {
69 | t.Fatal("Int64 must be valid")
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/nullgopher.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/string.go:
--------------------------------------------------------------------------------
1 | package null
2 |
3 | import (
4 | "database/sql"
5 | "encoding/json"
6 | )
7 |
8 | // String is a nullable string type based on sql.NullString, that supports parsing to/from JSON.
9 | type String struct {
10 | sql.NullString
11 | }
12 |
13 | // NewString returns a new nullable String object.
14 | // This is equivalent to `null.String{sql.NullString{String: s, Valid: valid}}`.
15 | func NewString(s string, valid bool) String {
16 | return String{sql.NullString{String: s, Valid: valid}}
17 | }
18 |
19 | // MarshalJSON implements the encoding json interface.
20 | func (s String) MarshalJSON() ([]byte, error) {
21 | if s.Valid {
22 | return json.Marshal(s.String)
23 | }
24 |
25 | return json.Marshal(nil)
26 | }
27 |
28 | // UnmarshalJSON implements the encoding json interface.
29 | func (s *String) UnmarshalJSON(data []byte) error {
30 | var value *string
31 |
32 | if err := json.Unmarshal(data, &value); err != nil {
33 | return err
34 | }
35 |
36 | if value != nil {
37 | s.SetValid(*value)
38 | } else {
39 | s.SetNil()
40 | }
41 |
42 | return nil
43 | }
44 |
45 | // SetValid sets the value and valid to true.
46 | func (s *String) SetValid(value string) {
47 | s.String = value
48 | s.Valid = true
49 | }
50 |
51 | // SetNil sets the value to default and valid to false.
52 | func (s *String) SetNil() {
53 | s.String = ""
54 | s.Valid = false
55 | }
56 |
--------------------------------------------------------------------------------
/string_test.go:
--------------------------------------------------------------------------------
1 | package null
2 |
3 | import (
4 | "database/sql"
5 | "encoding/json"
6 | "testing"
7 | )
8 |
9 | type testString struct {
10 | Value String `json:"value"`
11 | }
12 |
13 | func TestNewString(t *testing.T) {
14 | value := NewString("test", true)
15 |
16 | if value.String != "test" || !value.Valid {
17 | t.Fatal("New String must have value and be valid")
18 | }
19 | }
20 |
21 | func TestMarshalString(t *testing.T) {
22 | value := String{sql.NullString{String: "test", Valid: true}}
23 |
24 | if data, err := json.Marshal(value); err != nil || string(data) != "\"test\"" {
25 | t.Fatalf("String must be marshalled to value, but was %v %v", err, string(data))
26 | }
27 |
28 | value.Valid = false
29 |
30 | if data, err := json.Marshal(value); err != nil || string(data) != "null" {
31 | t.Fatalf("String must be marshalled to null, but was %v %v", err, string(data))
32 | }
33 | }
34 |
35 | func TestUnmarshalString(t *testing.T) {
36 | str := `{"value": "test"}`
37 | var value testString
38 |
39 | if err := json.Unmarshal([]byte(str), &value); err != nil {
40 | t.Fatalf("String must be unmarshalled to value, but was %v", err)
41 | }
42 |
43 | if !value.Value.Valid || value.Value.String != "test" {
44 | t.Fatalf("Unmarshalled null string must be valid, but was %v", value.Value)
45 | }
46 |
47 | str = `{"value": null}`
48 |
49 | if err := json.Unmarshal([]byte(str), &value); err != nil {
50 | t.Fatalf("String must be unmarshalled to null, but was %v", err)
51 | }
52 |
53 | if value.Value.Valid {
54 | t.Fatal("Unmarshalled null string must be invalid")
55 | }
56 | }
57 |
58 | func TestGettersSettersString(t *testing.T) {
59 | value := NewString("test", true)
60 | value.SetNil()
61 |
62 | if value.String != "" || value.Valid {
63 | t.Fatal("String must be nil")
64 | }
65 |
66 | value.SetValid("test")
67 |
68 | if value.String != "test" || !value.Valid {
69 | t.Fatal("String must be valid")
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/time.go:
--------------------------------------------------------------------------------
1 | package null
2 |
3 | import (
4 | "database/sql"
5 | "database/sql/driver"
6 | "encoding/json"
7 | "errors"
8 | "time"
9 | )
10 |
11 | // Time is a nullable time.Time, that supports parsing to/from JSON.
12 | type Time struct {
13 | sql.NullTime
14 | }
15 |
16 | // NewTime returns a new nullable time.Time object.
17 | // This is equivalent to `null.Time{Time: t, Valid: valid}`.
18 | func NewTime(t time.Time, valid bool) Time {
19 | return Time{sql.NullTime{Time: t, Valid: valid}}
20 | }
21 |
22 | // MarshalJSON implements the encoding json interface.
23 | func (t Time) MarshalJSON() ([]byte, error) {
24 | if t.Valid {
25 | return json.Marshal(t.Time)
26 | }
27 |
28 | return json.Marshal(nil)
29 | }
30 |
31 | // UnmarshalJSON implements the encoding json interface.
32 | func (t *Time) UnmarshalJSON(data []byte) error {
33 | var value time.Time
34 |
35 | if err := json.Unmarshal(data, &value); err != nil {
36 | return err
37 | }
38 |
39 | if !value.IsZero() {
40 | t.SetValid(value)
41 | } else {
42 | t.SetNil()
43 | }
44 |
45 | return nil
46 | }
47 |
48 | // Scan implements the Scanner interface.
49 | func (t *Time) Scan(value interface{}) error {
50 | if value == nil {
51 | t.SetNil()
52 | return nil
53 | }
54 |
55 | v, ok := value.(time.Time)
56 |
57 | if !ok {
58 | return errors.New("unexpected type")
59 | }
60 |
61 | t.SetValid(v)
62 | return nil
63 | }
64 |
65 | // Value implements the driver Valuer interface.
66 | func (t Time) Value() (driver.Value, error) {
67 | if !t.Valid {
68 | return nil, nil
69 | }
70 |
71 | return t.Time, nil
72 | }
73 |
74 | // SetValid sets the value and valid to true.
75 | func (t *Time) SetValid(value time.Time) {
76 | t.Time = value
77 | t.Valid = true
78 | }
79 |
80 | // SetNil sets the value to default and valid to false.
81 | func (t *Time) SetNil() {
82 | t.Time = time.Time{}
83 | t.Valid = false
84 | }
85 |
--------------------------------------------------------------------------------
/time_test.go:
--------------------------------------------------------------------------------
1 | package null
2 |
3 | import (
4 | "database/sql"
5 | "encoding/json"
6 | "testing"
7 | "time"
8 | )
9 |
10 | type testTime struct {
11 | Value Time `json:"value"`
12 | }
13 |
14 | func TestNewTime(t *testing.T) {
15 | value := NewTime(time.Now(), true)
16 |
17 | if value.Time.Equal(time.Time{}) || !value.Valid {
18 | t.Fatal("New Time must have value and be valid")
19 | }
20 | }
21 |
22 | func TestMarshalTime(t *testing.T) {
23 | now := time.Now()
24 | nowStrBytes, _ := json.Marshal(now)
25 | nowStr := string(nowStrBytes)
26 | value := Time{sql.NullTime{Time: now, Valid: true}}
27 |
28 | if data, err := json.Marshal(value); err != nil || string(data) != nowStr {
29 | t.Fatalf("Time must be marshalled to value, but was %v %v", err, string(data))
30 | }
31 |
32 | value.Valid = false
33 |
34 | if data, err := json.Marshal(value); err != nil || string(data) != "null" {
35 | t.Fatalf("Time must be marshalled to null, but was %v %v", err, string(data))
36 | }
37 | }
38 |
39 | func TestUnmarshalTime(t *testing.T) {
40 | now := time.Now()
41 | nowStrBytes, _ := json.Marshal(now)
42 | nowStr := string(nowStrBytes)
43 | str := `{"value": ` + nowStr + `}`
44 | var value testTime
45 |
46 | if err := json.Unmarshal([]byte(str), &value); err != nil {
47 | t.Fatalf("Time must be unmarshalled to value, but was %v", err)
48 | }
49 |
50 | if !value.Value.Valid || !value.Value.Time.Equal(now) {
51 | t.Fatalf("Unmarshalled null Time must be valid, but was %v", value.Value)
52 | }
53 |
54 | str = `{"value": null}`
55 |
56 | if err := json.Unmarshal([]byte(str), &value); err != nil {
57 | t.Fatalf("Time must be unmarshalled to null, but was %v", err)
58 | }
59 |
60 | if value.Value.Valid {
61 | t.Fatal("Unmarshalled null Time must be invalid")
62 | }
63 | }
64 |
65 | func TestScanTime(t *testing.T) {
66 | now := time.Now()
67 | str := "test"
68 | var value Time
69 |
70 | if err := value.Scan(&str); err == nil || err.Error() != "unexpected type" {
71 | t.Fatalf("Time must return error, but was: %v", err)
72 | }
73 |
74 | if err := value.Scan(now); err != nil {
75 | t.Fatalf("Time must be scanned, but was: %v", err)
76 | }
77 |
78 | if !value.Time.Equal(now) {
79 | t.Fatalf("Scanned time must be equal to input, but was: %v == %v", value.Time, now)
80 | }
81 | }
82 |
83 | func TestValueTime(t *testing.T) {
84 | value := NewTime(time.Now(), true)
85 | out, err := value.Value()
86 |
87 | if err != nil || out == nil {
88 | t.Fatalf("Time must return value, but was: %v", err)
89 | }
90 | }
91 |
92 | func TestGettersSettersTime(t *testing.T) {
93 | value := NewTime(time.Now(), true)
94 | value.SetNil()
95 |
96 | if !value.Time.Equal(time.Time{}) || value.Valid {
97 | t.Fatal("Time must be nil")
98 | }
99 |
100 | value.SetValid(time.Now())
101 |
102 | if value.Time.Equal(time.Time{}) || !value.Valid {
103 | t.Fatal("Time must be valid")
104 | }
105 | }
106 |
--------------------------------------------------------------------------------