Skip to content

Instantly share code, notes, and snippets.

@podhmo
Last active December 29, 2022 13:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save podhmo/436de31a6fd08e21eb0f629239557d80 to your computer and use it in GitHub Desktop.
Save podhmo/436de31a6fd08e21eb0f629239557d80 to your computer and use it in GitHub Desktop.
package nullable
import (
"bytes"
"database/sql/driver"
"encoding/json"
"fmt"
"reflect"
"time"
_ "unsafe"
)
//go:linkname database_sql__convertAssign database/sql.convertAssign
func database_sql__convertAssign(dest, src any) error
type constraint interface {
~string | ~[]byte |
~int64 | ~int32 | ~int | ~int16 | ~int8 |
~uint64 | ~uint32 | ~uint | ~uint16 | ~uint8 | ~float64 |
~bool | time.Time
comparable
}
type Type[T constraint] struct {
value T
Valid bool
}
func (t *Type[T]) UnmarshalJSON(b []byte) error {
if b == nil || bytes.Equal(nullValue, b) {
return nil
}
t.Valid = true
return json.Unmarshal(b, &t.value) // TODO:performance improvement
}
func (t Type[T]) MarshalJSON() ([]byte, error) {
if !t.Valid {
return nullValue, nil
}
return json.Marshal(t.value) // TODO: performance improvement
}
func (t *Type[T]) Scan(value any) error {
if value == nil {
var z T
t.value = z
t.Valid = false
return nil
}
t.Valid = true
return database_sql__convertAssign(&t.value, value)
}
func (t Type[T]) Value() (driver.Value, error) {
if !t.Valid {
return nil, nil
}
// ONLY: int64 | float64 | bool | []byte | string | time.Time
rv := reflect.ValueOf(t.value)
switch rv.Kind() {
case reflect.Bool:
return rv.Bool(), nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return int64(rv.Int()), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32:
return int64(rv.Uint()), nil
case reflect.Uint64:
return int64(rv.Uint()), nil // XXX: overflow
case reflect.Float32, reflect.Float64:
return float64(rv.Float()), nil
case reflect.Slice:
if rv.Type().Elem() == rBytesType {
return rv.Bytes(), nil
}
return nil, fmt.Errorf("unexpected type: %v", rv.Type())
case reflect.String:
return rv.String(), nil
// case reflect.Complex64:
// case reflect.Complex128:
// case reflect.Array:
// case reflect.Chan:
// case reflect.Func:
// case reflect.Interface:
// case reflect.Map:
// case reflect.Pointer:
// case reflect.Struct:
// case reflect.UnsafePointer:
default:
if rv.Type() == rTimeType {
return rv.Interface().(time.Time), nil
}
return nil, fmt.Errorf("unexpected type: %v", rv.Type())
}
}
func New[T constraint](value T) Type[T] {
return Type[T]{
value: value,
Valid: true,
}
}
var nullValue = []byte(`null`)
var rBytesType = reflect.TypeOf(func([]byte) {}).In(0)
var rTimeType = reflect.TypeOf(func(time.Time) {}).In(0)
package nullable_test
import (
"bytes"
"encoding/json"
"errors"
"strings"
"testing"
"github.com/podhmo/individual-sandbox/daily/20221229/example_go/nullable"
)
func jsonNormalize(t *testing.T, val []byte) []byte {
buf := new(bytes.Buffer)
if err := json.Compact(buf, val); err != nil {
t.Fatalf("unexpected json: %v", err)
}
return buf.Bytes()
}
func TestJSON(t *testing.T) {
decodeJSON := func(input string, ob any) (any, error) {
if err := json.NewDecoder(strings.NewReader(input)).Decode(ob); err != nil {
return nil, err
}
return ob, nil
}
cases := []struct {
msg string
input string
want string
decode func(input string) (any, error)
assertDecodeError func(*testing.T, error)
}{
{
msg: "ok-string",
input: `{"string": "foo", "zero_string": "", "null_string": null}`,
want: `{"string": "foo", "zero_string": "", "null_string": null, "omitempty_string": null}`,
decode: func(input string) (any, error) {
var ob struct {
String nullable.Type[string] `json:"string"`
ZeroString nullable.Type[string] `json:"zero_string"`
NullString nullable.Type[string] `json:"null_string"`
OmitemptyString nullable.Type[string] `json:"omitempty_string,omitempty"`
}
return decodeJSON(input, &ob)
},
},
{
msg: "ok-int32",
input: `{"int32": 100, "zero_int32": 0, "null_int32": null}`,
want: `{"int32": 100, "zero_int32": 0, "null_int32": null, "omitempty_int32": null}`,
decode: func(input string) (any, error) {
var ob struct {
Int32 nullable.Type[int32] `json:"int32"`
Zeroint32 nullable.Type[int32] `json:"zero_int32"`
Nullint32 nullable.Type[int32] `json:"null_int32"`
Omitemptyint32 nullable.Type[int32] `json:"omitempty_int32,omitempty"`
}
return decodeJSON(input, &ob)
},
},
{
msg: "ng-int32",
input: `{"int32": "foo"}`,
decode: func(input string) (any, error) {
var ob struct {
Int32 nullable.Type[int32] `json:"int32"`
}
return decodeJSON(input, &ob)
},
assertDecodeError: func(t *testing.T, err error) {
want := &json.UnmarshalTypeError{}
if !errors.As(err, &want) {
t.Errorf("mismatch decode error: %T != %T", err, want)
}
},
},
{
msg: "ok-newType",
input: `{"ordering": "desc", "zero_ordering": "", "null_ordering": null}`,
want: `{"ordering": "desc", "zero_ordering": "", "null_ordering": null, "omitempty_ordering": null}`,
decode: func(input string) (any, error) {
type Ordering string
var ob struct {
Ordering nullable.Type[Ordering] `json:"ordering"`
ZeroOrdering nullable.Type[Ordering] `json:"zero_ordering"`
NullOrdering nullable.Type[Ordering] `json:"null_ordering"`
OmitemptyOrdering nullable.Type[Ordering] `json:"omitempty_ordering,omitempty"`
}
return decodeJSON(input, &ob)
},
},
}
for _, c := range cases {
c := c
t.Run(c.msg, func(t *testing.T) {
buf := new(bytes.Buffer)
t.Logf(" input json: %s", c.input)
ob, err := c.decode(c.input)
if c.assertDecodeError != nil {
if err == nil {
t.Fatalf("must be error")
}
c.assertDecodeError(t, err)
return
}
if err != nil {
t.Fatalf("unexpected decode error: %+v", err)
}
if err := json.NewEncoder(buf).Encode(ob); err != nil {
t.Errorf("unexpected encode error: %+v", err)
}
t.Logf("output json: %s", buf.String())
if want, got := jsonNormalize(t, []byte(c.want)), jsonNormalize(t, buf.Bytes()); !bytes.Equal(want, got) {
t.Errorf("mismatch json\nwant:\n\t%q\ngot:\n\t%q", string(want), string(got))
}
})
}
}
@podhmo
Copy link
Author

podhmo commented Dec 29, 2022

当たり前だけど underlying typeのあれがアレなときに型を書かなければいけない。literal typeが欲しくなる。

@podhmo
Copy link
Author

podhmo commented Dec 29, 2022

  • 📝 this is not monadic library

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment