Skip to content

Commit 25a9e71

Browse files
authored
Merge pull request #77 from guregu/v5-dev
v5: More types, generics
2 parents 21596e8 + 782c7fe commit 25a9e71

31 files changed

+2134
-560
lines changed

.github/workflows/test.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
name: Deploy
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
spin:
7+
runs-on: ubuntu-latest
8+
name: Test
9+
steps:
10+
- uses: actions/checkout@v4
11+
- uses: actions/setup-go@v5
12+
with:
13+
go-version: 'stable'
14+
- run: go test -v -race -coverpkg=./... ./...

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
coverage.out
1+
cover*.out
22
.idea/
3+
.DS_Store

README.md

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
## null [![GoDoc](https://godoc.org/github.com/guregu/null?status.svg)](https://godoc.org/github.com/guregu/null) [![CircleCI](https://circleci.com/gh/guregu/null.svg?style=svg)](https://circleci.com/gh/guregu/null)
2-
`import "gopkg.in/guregu/null.v4"`
1+
## null [![GoDoc](https://godoc.org/github.com/guregu/null/v5?status.svg)](https://godoc.org/github.com/guregu/null/v5)
2+
`import "github.com/guregu/null/v5"`
33

44
null is a library with reasonable options for dealing with nullable SQL and JSON values
55

@@ -9,20 +9,23 @@ Types in `null` will only be considered null on null input, and will JSON encode
99

1010
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.
1111

12-
All types implement `sql.Scanner` and `driver.Valuer`, so you can use this library in place of `sql.NullXXX`.
13-
All types also implement: `encoding.TextMarshaler`, `encoding.TextUnmarshaler`, `json.Marshaler`, and `json.Unmarshaler`. A null object's `MarshalText` will return a blank string.
12+
#### Interfaces
1413

15-
### null package
14+
- All types implement `sql.Scanner` and `driver.Valuer`, so you can use this library in place of `sql.NullXXX`.
15+
- All types also implement `json.Marshaler` and `json.Unmarshaler`, so you can marshal them to their native JSON representation.
16+
- All non-generic types implement `encoding.TextMarshaler`, `encoding.TextUnmarshaler`. A null object's `MarshalText` will return a blank string.
1617

17-
`import "gopkg.in/guregu/null.v4"`
18+
## null package
19+
20+
`import "github.com/guregu/null/v5"`
1821

1922
#### null.String
2023
Nullable string.
2124

2225
Marshals to JSON null if SQL source data is null. Zero (blank) input will not produce a null String.
2326

24-
#### null.Int
25-
Nullable int64.
27+
#### null.Int, null.Int32, null.Int16, null.Byte
28+
Nullable int64/int32/int16/byte.
2629

2730
Marshals to JSON null if SQL source data is null. Zero input will not produce a null Int.
2831

@@ -40,17 +43,22 @@ Marshals to JSON null if SQL source data is null. False input will not produce a
4043

4144
Marshals to JSON null if SQL source data is null. Zero input will not produce a null Time.
4245

43-
### zero package
46+
#### null.Value
47+
Generic nullable value.
48+
49+
Will marshal to JSON null if SQL source data is null. Does not implement `encoding.TextMarshaler`.
50+
51+
## zero package
4452

45-
`import "gopkg.in/guregu/null.v4/zero"`
53+
`import "github.com/guregu/null/v5/zero"`
4654

4755
#### zero.String
4856
Nullable string.
4957

5058
Will marshal to a blank string if null. Blank string input produces a null String. Null values and zero values are considered equivalent.
5159

52-
#### zero.Int
53-
Nullable int64.
60+
#### zero.Int, zero.Int32, zero.Int16, zero.Byte
61+
Nullable int64/int32/int16/byte.
5462

5563
Will marshal to 0 if null. 0 produces a null Int. Null values and zero values are considered equivalent.
5664

@@ -65,17 +73,35 @@ Nullable bool.
6573
Will marshal to false if null. `false` produces a null Float. Null values and zero values are considered equivalent.
6674

6775
#### zero.Time
76+
Nullable time.
6877

6978
Will marshal to the zero time if null. Uses `time.Time`'s marshaler.
7079

71-
### Can you add support for other types?
80+
#### zero.Value[`T`]
81+
Generic nullable value.
82+
83+
Will marshal to zero value if null. `T` is required to be a comparable type. Does not implement `encoding.TextMarshaler`.
84+
85+
## About
86+
87+
### Q&A
88+
89+
#### Can you add support for other types?
7290
This package is intentionally limited in scope. It will only support the types that [`driver.Value`](https://godoc.org/database/sql/driver#Value) supports. Feel free to fork this and add more types if you want.
7391

74-
### Can you add a feature that ____?
92+
#### Can you add a feature that ____?
7593
This package isn't intended to be a catch-all data-wrangling package. It is essentially finished. If you have an idea for a new feature, feel free to open an issue to talk about it or fork this package, but don't expect this to do everything.
7694

7795
### Package history
78-
*As of v4*, unmarshaling from JSON `sql.NullXXX` JSON objects (ex. `{"Int64": 123, "Valid": true}`) is no longer supported. It's unlikely many people used this, but if you need it, use v3.
96+
97+
#### v5
98+
- Now a Go module under the path `github.com/guregu/null/v5`
99+
- Added missing types from `database/sql`: `Int32, Int16, Byte`
100+
- Added generic `Value[T]` embedding `sql.Null[T]`
101+
102+
#### v4
103+
- Available at `gopkg.in/guregu/null.v4`
104+
- Unmarshaling from JSON `sql.NullXXX` JSON objects (e.g. `{"Int64": 123, "Valid": true}`) is no longer supported. It's unlikely many people used this, but if you need it, use v3.
79105

80106
### Bugs
81107
`json`'s `",omitempty"` struct tag does not work correctly right now. It will never omit a null or empty String. This might be [fixed eventually](https://github.com/golang/go/issues/11939).

bool.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package null
22

33
import (
4-
"bytes"
54
"database/sql"
65
"encoding/json"
76
"errors"
@@ -47,7 +46,7 @@ func (b Bool) ValueOrZero() bool {
4746
// It supports number and null input.
4847
// 0 will not be considered a null Bool.
4948
func (b *Bool) UnmarshalJSON(data []byte) error {
50-
if bytes.Equal(data, nullBytes) {
49+
if len(data) > 0 && data[0] == 'n' {
5150
b.Valid = false
5251
return nil
5352
}

byte.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package null
2+
3+
import (
4+
"database/sql"
5+
"strconv"
6+
7+
"github.com/guregu/null/v5/internal"
8+
)
9+
10+
// Byte is an nullable byte.
11+
// It does not consider zero values to be null.
12+
// It will decode to null, not zero, if null.
13+
type Byte struct {
14+
sql.NullByte
15+
}
16+
17+
// NewByte creates a new Byte.
18+
func NewByte(b byte, valid bool) Byte {
19+
return Byte{
20+
NullByte: sql.NullByte{
21+
Byte: b,
22+
Valid: valid,
23+
},
24+
}
25+
}
26+
27+
// ByteFrom creates a new Byte that will always be valid.
28+
func ByteFrom(b byte) Byte {
29+
return NewByte(b, true)
30+
}
31+
32+
// ByteFromPtr creates a new Byte that be null if i is nil.
33+
func ByteFromPtr(b *byte) Byte {
34+
if b == nil {
35+
return NewByte(0, false)
36+
}
37+
return NewByte(*b, true)
38+
}
39+
40+
// ValueOrZero returns the inner value if valid, otherwise zero.
41+
func (b Byte) ValueOrZero() byte {
42+
if !b.Valid {
43+
return 0
44+
}
45+
return b.Byte
46+
}
47+
48+
// UnmarshalJSON implements json.Unmarshaler.
49+
// It supports number, string, and null input.
50+
// 0 will not be considered a null Byte.
51+
func (b *Byte) UnmarshalJSON(data []byte) error {
52+
return internal.UnmarshalIntJSON(data, &b.Byte, &b.Valid, 8, strconv.ParseUint)
53+
}
54+
55+
// UnmarshalText implements encoding.TextUnmarshaler.
56+
// It will unmarshal to a null Byte if the input is blank.
57+
// It will return an error if the input is not an integer, blank, or "null".
58+
func (b *Byte) UnmarshalText(text []byte) error {
59+
return internal.UnmarshalIntText(text, &b.Byte, &b.Valid, 8, strconv.ParseUint)
60+
}
61+
62+
// MarshalJSON implements json.Marshaler.
63+
// It will encode null if this Byte is null.
64+
func (b Byte) MarshalJSON() ([]byte, error) {
65+
if !b.Valid {
66+
return []byte("null"), nil
67+
}
68+
return []byte(strconv.FormatInt(int64(b.Byte), 10)), nil
69+
}
70+
71+
// MarshalText implements encoding.TextMarshaler.
72+
// It will encode a blank string if this Byte is null.
73+
func (b Byte) MarshalText() ([]byte, error) {
74+
if !b.Valid {
75+
return []byte{}, nil
76+
}
77+
return []byte(strconv.FormatInt(int64(b.Byte), 10)), nil
78+
}
79+
80+
// SetValid changes this Byte's value and also sets it to be non-null.
81+
func (b *Byte) SetValid(n byte) {
82+
b.Byte = n
83+
b.Valid = true
84+
}
85+
86+
// Ptr returns a pointer to this Byte's value, or a nil pointer if this Byte is null.
87+
func (b Byte) Ptr() *byte {
88+
if !b.Valid {
89+
return nil
90+
}
91+
return &b.Byte
92+
}
93+
94+
// IsZero returns true for invalid Bytes, for future omitempty support (Go 1.4?)
95+
// A non-null Byte with a 0 value will not be considered zero.
96+
func (b Byte) IsZero() bool {
97+
return !b.Valid
98+
}
99+
100+
// Equal returns true if both ints have the same value or are both null.
101+
func (b Byte) Equal(other Byte) bool {
102+
return b.Valid == other.Valid && (!b.Valid || b.Byte == other.Byte)
103+
}
104+
105+
func (b Byte) value() (int64, bool) {
106+
return int64(b.Byte), b.Valid
107+
}

float.go

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
package null
22

33
import (
4-
"bytes"
54
"database/sql"
65
"encoding/json"
7-
"errors"
86
"fmt"
97
"math"
108
"reflect"
119
"strconv"
10+
11+
"github.com/guregu/null/v5/internal"
1212
)
1313

1414
// Float is a nullable float64.
@@ -53,35 +53,7 @@ func (f Float) ValueOrZero() float64 {
5353
// It supports number and null input.
5454
// 0 will not be considered a null Float.
5555
func (f *Float) UnmarshalJSON(data []byte) error {
56-
if bytes.Equal(data, nullBytes) {
57-
f.Valid = false
58-
return nil
59-
}
60-
61-
if err := json.Unmarshal(data, &f.Float64); err != nil {
62-
var typeError *json.UnmarshalTypeError
63-
if errors.As(err, &typeError) {
64-
// special case: accept string input
65-
if typeError.Value != "string" {
66-
return fmt.Errorf("null: JSON input is invalid type (need float or string): %w", err)
67-
}
68-
var str string
69-
if err := json.Unmarshal(data, &str); err != nil {
70-
return fmt.Errorf("null: couldn't unmarshal number string: %w", err)
71-
}
72-
n, err := strconv.ParseFloat(str, 64)
73-
if err != nil {
74-
return fmt.Errorf("null: couldn't convert string to float: %w", err)
75-
}
76-
f.Float64 = n
77-
f.Valid = true
78-
return nil
79-
}
80-
return fmt.Errorf("null: couldn't unmarshal JSON: %w", err)
81-
}
82-
83-
f.Valid = true
84-
return nil
56+
return internal.UnmarshalFloatJSON(data, &f.Float64, &f.Valid)
8557
}
8658

8759
// UnmarshalText implements encoding.TextUnmarshaler.
@@ -94,7 +66,7 @@ func (f *Float) UnmarshalText(text []byte) error {
9466
return nil
9567
}
9668
var err error
97-
f.Float64, err = strconv.ParseFloat(string(text), 64)
69+
f.Float64, err = strconv.ParseFloat(str, 64)
9870
if err != nil {
9971
return fmt.Errorf("null: couldn't unmarshal text: %w", err)
10072
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/guregu/null/v5
2+
3+
go 1.21.4

0 commit comments

Comments
 (0)