Skip to content

Commit

Permalink
Merge pull request #77 from guregu/v5-dev
Browse files Browse the repository at this point in the history
v5: More types, generics
  • Loading branch information
guregu committed Feb 11, 2024
2 parents 21596e8 + 782c7fe commit 25a9e71
Show file tree
Hide file tree
Showing 31 changed files with 2,134 additions and 560 deletions.
14 changes: 14 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Deploy

on: [push, pull_request]

jobs:
spin:
runs-on: ubuntu-latest
name: Test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 'stable'
- run: go test -v -race -coverpkg=./... ./...
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
coverage.out
cover*.out
.idea/
.DS_Store
56 changes: 41 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
## 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)
`import "gopkg.in/guregu/null.v4"`
## null [![GoDoc](https://godoc.org/github.com/guregu/null/v5?status.svg)](https://godoc.org/github.com/guregu/null/v5)
`import "github.com/guregu/null/v5"`

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

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

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`. A null object's `MarshalText` will return a blank string.
#### Interfaces

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

`import "gopkg.in/guregu/null.v4"`
## null package

`import "github.com/guregu/null/v5"`

#### null.String
Nullable string.

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

#### null.Int
Nullable int64.
#### null.Int, null.Int32, null.Int16, null.Byte
Nullable int64/int32/int16/byte.

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

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

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

### zero package
#### null.Value
Generic nullable value.

Will marshal to JSON null if SQL source data is null. Does not implement `encoding.TextMarshaler`.

## zero package

`import "gopkg.in/guregu/null.v4/zero"`
`import "github.com/guregu/null/v5/zero"`

#### 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.

#### zero.Int
Nullable int64.
#### zero.Int, zero.Int32, zero.Int16, zero.Byte
Nullable int64/int32/int16/byte.

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

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

#### zero.Time
Nullable time.

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

### Can you add support for other types?
#### zero.Value[`T`]
Generic nullable value.

Will marshal to zero value if null. `T` is required to be a comparable type. Does not implement `encoding.TextMarshaler`.

## About

### Q&A

#### Can you add support for other types?
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.

### Can you add a feature that ____?
#### Can you add a feature that ____?
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.

### Package history
*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.

#### v5
- Now a Go module under the path `github.com/guregu/null/v5`
- Added missing types from `database/sql`: `Int32, Int16, Byte`
- Added generic `Value[T]` embedding `sql.Null[T]`

#### v4
- Available at `gopkg.in/guregu/null.v4`
- 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.

### Bugs
`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).
Expand Down
3 changes: 1 addition & 2 deletions bool.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package null

import (
"bytes"
"database/sql"
"encoding/json"
"errors"
Expand Down Expand Up @@ -47,7 +46,7 @@ func (b Bool) ValueOrZero() bool {
// It supports number and null input.
// 0 will not be considered a null Bool.
func (b *Bool) UnmarshalJSON(data []byte) error {
if bytes.Equal(data, nullBytes) {
if len(data) > 0 && data[0] == 'n' {
b.Valid = false
return nil
}
Expand Down
107 changes: 107 additions & 0 deletions byte.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package null

import (
"database/sql"
"strconv"

"github.com/guregu/null/v5/internal"
)

// Byte is an nullable byte.
// It does not consider zero values to be null.
// It will decode to null, not zero, if null.
type Byte struct {
sql.NullByte
}

// NewByte creates a new Byte.
func NewByte(b byte, valid bool) Byte {
return Byte{
NullByte: sql.NullByte{
Byte: b,
Valid: valid,
},
}
}

// ByteFrom creates a new Byte that will always be valid.
func ByteFrom(b byte) Byte {
return NewByte(b, true)
}

// ByteFromPtr creates a new Byte that be null if i is nil.
func ByteFromPtr(b *byte) Byte {
if b == nil {
return NewByte(0, false)
}
return NewByte(*b, true)
}

// ValueOrZero returns the inner value if valid, otherwise zero.
func (b Byte) ValueOrZero() byte {
if !b.Valid {
return 0
}
return b.Byte
}

// UnmarshalJSON implements json.Unmarshaler.
// It supports number, string, and null input.
// 0 will not be considered a null Byte.
func (b *Byte) UnmarshalJSON(data []byte) error {
return internal.UnmarshalIntJSON(data, &b.Byte, &b.Valid, 8, strconv.ParseUint)
}

// UnmarshalText implements encoding.TextUnmarshaler.
// It will unmarshal to a null Byte if the input is blank.
// It will return an error if the input is not an integer, blank, or "null".
func (b *Byte) UnmarshalText(text []byte) error {
return internal.UnmarshalIntText(text, &b.Byte, &b.Valid, 8, strconv.ParseUint)
}

// MarshalJSON implements json.Marshaler.
// It will encode null if this Byte is null.
func (b Byte) MarshalJSON() ([]byte, error) {
if !b.Valid {
return []byte("null"), nil
}
return []byte(strconv.FormatInt(int64(b.Byte), 10)), nil
}

// MarshalText implements encoding.TextMarshaler.
// It will encode a blank string if this Byte is null.
func (b Byte) MarshalText() ([]byte, error) {
if !b.Valid {
return []byte{}, nil
}
return []byte(strconv.FormatInt(int64(b.Byte), 10)), nil
}

// SetValid changes this Byte's value and also sets it to be non-null.
func (b *Byte) SetValid(n byte) {
b.Byte = n
b.Valid = true
}

// Ptr returns a pointer to this Byte's value, or a nil pointer if this Byte is null.
func (b Byte) Ptr() *byte {
if !b.Valid {
return nil
}
return &b.Byte
}

// IsZero returns true for invalid Bytes, for future omitempty support (Go 1.4?)
// A non-null Byte with a 0 value will not be considered zero.
func (b Byte) IsZero() bool {
return !b.Valid
}

// Equal returns true if both ints have the same value or are both null.
func (b Byte) Equal(other Byte) bool {
return b.Valid == other.Valid && (!b.Valid || b.Byte == other.Byte)
}

func (b Byte) value() (int64, bool) {
return int64(b.Byte), b.Valid
}
36 changes: 4 additions & 32 deletions float.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package null

import (
"bytes"
"database/sql"
"encoding/json"
"errors"
"fmt"
"math"
"reflect"
"strconv"

"github.com/guregu/null/v5/internal"
)

// Float is a nullable float64.
Expand Down Expand Up @@ -53,35 +53,7 @@ func (f Float) ValueOrZero() float64 {
// It supports number and null input.
// 0 will not be considered a null Float.
func (f *Float) UnmarshalJSON(data []byte) error {
if bytes.Equal(data, nullBytes) {
f.Valid = false
return nil
}

if err := json.Unmarshal(data, &f.Float64); err != nil {
var typeError *json.UnmarshalTypeError
if errors.As(err, &typeError) {
// special case: accept string input
if typeError.Value != "string" {
return fmt.Errorf("null: JSON input is invalid type (need float or string): %w", err)
}
var str string
if err := json.Unmarshal(data, &str); err != nil {
return fmt.Errorf("null: couldn't unmarshal number string: %w", err)
}
n, err := strconv.ParseFloat(str, 64)
if err != nil {
return fmt.Errorf("null: couldn't convert string to float: %w", err)
}
f.Float64 = n
f.Valid = true
return nil
}
return fmt.Errorf("null: couldn't unmarshal JSON: %w", err)
}

f.Valid = true
return nil
return internal.UnmarshalFloatJSON(data, &f.Float64, &f.Valid)
}

// UnmarshalText implements encoding.TextUnmarshaler.
Expand All @@ -94,7 +66,7 @@ func (f *Float) UnmarshalText(text []byte) error {
return nil
}
var err error
f.Float64, err = strconv.ParseFloat(string(text), 64)
f.Float64, err = strconv.ParseFloat(str, 64)
if err != nil {
return fmt.Errorf("null: couldn't unmarshal text: %w", err)
}
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/guregu/null/v5

go 1.21.4
Loading

0 comments on commit 25a9e71

Please sign in to comment.