generic: emulate nullable string behaviour
This fills the gap for nullable strings returned by the API. Signed-off-by: Yonah <contrib@gensokyo.uk>
This commit is contained in:
parent
7dd470f9ae
commit
c2c4171a3a
23
generic.go
23
generic.go
@ -13,6 +13,29 @@ type Response[T any] struct {
|
|||||||
Data T `json:"data"`
|
Data T `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NullableString is a JSON string where its zero value behaves like null.
|
||||||
|
type NullableString string
|
||||||
|
|
||||||
|
func (s *NullableString) MarshalJSON() ([]byte, error) {
|
||||||
|
if *s == "" {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
return json.Marshal(string(*s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NullableString) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
var v *string
|
||||||
|
err = json.Unmarshal(data, &v)
|
||||||
|
if err == nil {
|
||||||
|
if v != nil {
|
||||||
|
*s = NullableString(*v)
|
||||||
|
} else {
|
||||||
|
*s = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// StringInt is a JSON string representing an integer.
|
// StringInt is a JSON string representing an integer.
|
||||||
type StringInt int
|
type StringInt int
|
||||||
|
|
||||||
|
@ -11,13 +11,17 @@ import (
|
|||||||
. "git.gensokyo.uk/yonah/monstersirenfetch"
|
. "git.gensokyo.uk/yonah/monstersirenfetch"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestNullableString(t *testing.T) {
|
||||||
|
checkJSONRoundTripM(t, []jsonRoundTripTestCase[NullableString]{
|
||||||
|
{"zero", nil, `null`, ""},
|
||||||
|
{"string", nil, `"null"`, "null"},
|
||||||
|
|
||||||
|
{"invalid json", newSyntaxError("unexpected end of JSON input", 5), `"null`, "\x00"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestStringInt(t *testing.T) {
|
func TestStringInt(t *testing.T) {
|
||||||
testCases := []struct {
|
checkJSONRoundTripM(t, []jsonRoundTripTestCase[StringInt]{
|
||||||
name string
|
|
||||||
wantErr error
|
|
||||||
data string
|
|
||||||
val int
|
|
||||||
}{
|
|
||||||
{"valid long", nil, `"3735928559"`, 0xdeadbeef},
|
{"valid long", nil, `"3735928559"`, 0xdeadbeef},
|
||||||
{"valid pad 4", nil, `"0000"`, 0},
|
{"valid pad 4", nil, `"0000"`, 0},
|
||||||
{"valid 4", nil, `"1000"`, 1e3},
|
{"valid 4", nil, `"1000"`, 1e3},
|
||||||
@ -26,13 +30,29 @@ func TestStringInt(t *testing.T) {
|
|||||||
|
|
||||||
{"invalid json", newSyntaxError("unexpected end of JSON input", 11), `"3735928559`, -1},
|
{"invalid json", newSyntaxError("unexpected end of JSON input", 11), `"3735928559`, -1},
|
||||||
{"invalid number", &strconv.NumError{Func: "Atoi", Num: ":3735928559", Err: strconv.ErrSyntax}, `":3735928559"`, -1},
|
{"invalid number", &strconv.NumError{Func: "Atoi", Num: ":3735928559", Err: strconv.ErrSyntax}, `":3735928559"`, -1},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newSyntaxError(msg string, offset int64) *json.SyntaxError {
|
||||||
|
e := &json.SyntaxError{Offset: offset}
|
||||||
|
msgV := reflect.ValueOf(e).Elem().FieldByName("msg")
|
||||||
|
reflect.NewAt(msgV.Type(), unsafe.Pointer(msgV.UnsafeAddr())).Elem().SetString(msg)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
type jsonRoundTripTestCase[T comparable] struct {
|
||||||
|
name string
|
||||||
|
wantErr error
|
||||||
|
data string
|
||||||
|
val T
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkJSONRoundTripM[T comparable](t *testing.T, testCases []jsonRoundTripTestCase[T]) {
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
if tc.wantErr == nil {
|
if tc.wantErr == nil {
|
||||||
t.Run("marshal", func(t *testing.T) {
|
t.Run("marshal", func(t *testing.T) {
|
||||||
v := StringInt(tc.val)
|
if got, err := json.Marshal(&tc.val); err != nil {
|
||||||
if got, err := json.Marshal(&v); err != nil {
|
|
||||||
t.Fatalf("Marshal: error = %v", err)
|
t.Fatalf("Marshal: error = %v", err)
|
||||||
} else if string(got) != tc.data {
|
} else if string(got) != tc.data {
|
||||||
t.Errorf("Marshal: %s, want %s", string(got), tc.data)
|
t.Errorf("Marshal: %s, want %s", string(got), tc.data)
|
||||||
@ -41,7 +61,7 @@ func TestStringInt(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t.Run("unmarshal", func(t *testing.T) {
|
t.Run("unmarshal", func(t *testing.T) {
|
||||||
var got StringInt
|
var got T
|
||||||
err := json.Unmarshal([]byte(tc.data), &got)
|
err := json.Unmarshal([]byte(tc.data), &got)
|
||||||
|
|
||||||
if !reflect.DeepEqual(err, tc.wantErr) {
|
if !reflect.DeepEqual(err, tc.wantErr) {
|
||||||
@ -49,8 +69,8 @@ func TestStringInt(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if tc.wantErr == nil {
|
if tc.wantErr == nil {
|
||||||
if int(got) != tc.val {
|
if got != tc.val {
|
||||||
t.Errorf("Unmarshal: %d, want %d", got, tc.val)
|
t.Errorf("Unmarshal: %v, want %v", got, tc.val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -58,13 +78,6 @@ func TestStringInt(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSyntaxError(msg string, offset int64) *json.SyntaxError {
|
|
||||||
e := &json.SyntaxError{Offset: offset}
|
|
||||||
msgV := reflect.ValueOf(e).Elem().FieldByName("msg")
|
|
||||||
reflect.NewAt(msgV.Type(), unsafe.Pointer(msgV.UnsafeAddr())).Elem().SetString(msg)
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkJSONRoundTrip[T any](t *testing.T, v T, data []byte) {
|
func checkJSONRoundTrip[T any](t *testing.T, v T, data []byte) {
|
||||||
t.Run("marshal", func(t *testing.T) {
|
t.Run("marshal", func(t *testing.T) {
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user