Last active
December 29, 2022 13:55
-
-
Save podhmo/436de31a6fd08e21eb0f629239557d80 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | |
} | |
}) | |
} | |
} |
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