Skip to content

Commit dfc71a4

Browse files
authored
Add Support For All SIgned and Unsigned Integers (#3787)
* feat(int): add support for all signed integers Signed-off-by: Rodney Osodo <socials@rodneyosodo.com> * feat(uint): add support for all unsigned integer types uint, uint8, uint16, uint32, uint64 * style: fix unnecessary conversion * fix(int): Potential overflow when casting uint64 to int64. If v > math.MaxInt64 the cast silently wraps around, turning a large positive value into a negative one and slipping past the size-checks in safeCastSignedNumber, yielding incorrect results instead of a deterministic overflow error. * fix(uint): wrong max constants for signed branches. safeCastUnsignedNumber treats "int8"/"int16"/"int32" the same as their unsigned counterparts and compares against math.MaxUint*. Casting a value 200 into int8 passes the check (200 ≤ 255) and returns int8(200) which is -56 after wrap-around. * fix: group cases in switch statement Signed-off-by: Rodney Osodo <socials@rodneyosodo.com> * test: remove unnecessary tests cases Signed-off-by: Rodney Osodo <socials@rodneyosodo.com> * test: add benchmark tests for UnmarshalUint and UnmarshalInt These are the results from my local setup: goos: linux goarch: amd64 pkg: github.com/99designs/gqlgen/graphql cpu: AMD Ryzen 7 7735HS with Radeon Graphics BenchmarkUnmarshalIntInitial-16 62162 19745 ns/op BenchmarkUnmarshalIntNew-16 53192 22884 ns/op BenchmarkUnmarshalUintInitial-16 18339 67228 ns/op BenchmarkUnmarshalUintNew-16 54229 22213 ns/op PASS ok github.com/99designs/gqlgen/graphql6.208s Signed-off-by: Rodney Osodo <socials@rodneyosodo.com> --------- Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>
1 parent 14f4431 commit dfc71a4

File tree

4 files changed

+617
-174
lines changed

4 files changed

+617
-174
lines changed

graphql/int.go

Lines changed: 89 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -5,83 +5,84 @@ import (
55
"fmt"
66
"io"
77
"math"
8+
"reflect"
89
"strconv"
910
)
1011

1112
func MarshalInt(i int) Marshaler {
1213
return WriterFunc(func(w io.Writer) {
13-
io.WriteString(w, strconv.Itoa(i))
14+
_, _ = io.WriteString(w, strconv.FormatInt(int64(i), 10))
1415
})
1516
}
1617

1718
func UnmarshalInt(v any) (int, error) {
18-
switch v := v.(type) {
19-
case string:
20-
return strconv.Atoi(v)
21-
case int:
22-
return v, nil
23-
case int64:
24-
return int(v), nil
25-
case json.Number:
26-
return strconv.Atoi(string(v))
27-
case nil:
28-
return 0, nil
29-
default:
30-
return 0, fmt.Errorf("%T is not an int", v)
31-
}
19+
return interfaceToSignedNumber[int](v)
3220
}
3321

34-
func MarshalInt64(i int64) Marshaler {
22+
func MarshalInt8(i int8) Marshaler {
3523
return WriterFunc(func(w io.Writer) {
36-
io.WriteString(w, strconv.FormatInt(i, 10))
24+
_, _ = io.WriteString(w, strconv.FormatInt(int64(i), 10))
3725
})
3826
}
3927

40-
func UnmarshalInt64(v any) (int64, error) {
41-
switch v := v.(type) {
42-
case string:
43-
return strconv.ParseInt(v, 10, 64)
44-
case int:
45-
return int64(v), nil
46-
case int64:
47-
return v, nil
48-
case json.Number:
49-
return strconv.ParseInt(string(v), 10, 64)
50-
case nil:
51-
return 0, nil
52-
default:
53-
return 0, fmt.Errorf("%T is not an int", v)
54-
}
28+
func UnmarshalInt8(v any) (int8, error) {
29+
return interfaceToSignedNumber[int8](v)
30+
}
31+
32+
func MarshalInt16(i int16) Marshaler {
33+
return WriterFunc(func(w io.Writer) {
34+
_, _ = io.WriteString(w, strconv.FormatInt(int64(i), 10))
35+
})
36+
}
37+
38+
func UnmarshalInt16(v any) (int16, error) {
39+
return interfaceToSignedNumber[int16](v)
5540
}
5641

5742
func MarshalInt32(i int32) Marshaler {
5843
return WriterFunc(func(w io.Writer) {
59-
io.WriteString(w, strconv.FormatInt(int64(i), 10))
44+
_, _ = io.WriteString(w, strconv.FormatInt(int64(i), 10))
6045
})
6146
}
6247

6348
func UnmarshalInt32(v any) (int32, error) {
49+
return interfaceToSignedNumber[int32](v)
50+
}
51+
52+
func MarshalInt64(i int64) Marshaler {
53+
return WriterFunc(func(w io.Writer) {
54+
_, _ = io.WriteString(w, strconv.FormatInt(i, 10))
55+
})
56+
}
57+
58+
func UnmarshalInt64(v any) (int64, error) {
59+
return interfaceToSignedNumber[int64](v)
60+
}
61+
62+
type number interface {
63+
int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64
64+
}
65+
66+
func interfaceToSignedNumber[N number](v any) (N, error) {
6467
switch v := v.(type) {
68+
case int, int8, int16, int32, int64:
69+
return safeCastSignedNumber[N](reflect.ValueOf(v).Int())
6570
case string:
6671
iv, err := strconv.ParseInt(v, 10, 64)
6772
if err != nil {
6873
return 0, err
6974
}
70-
return safeCastInt32(iv)
71-
case int:
72-
return safeCastInt32(int64(v))
73-
case int64:
74-
return safeCastInt32(v)
75+
return safeCastSignedNumber[N](iv)
7576
case json.Number:
7677
iv, err := strconv.ParseInt(string(v), 10, 64)
7778
if err != nil {
7879
return 0, err
7980
}
80-
return safeCastInt32(iv)
81+
return safeCastSignedNumber[N](iv)
8182
case nil:
8283
return 0, nil
8384
default:
84-
return 0, fmt.Errorf("%T is not an int", v)
85+
return 0, fmt.Errorf("%T is not an %T", v, N(0))
8586
}
8687
}
8788

@@ -98,27 +99,62 @@ func (e IntegerError) Error() string {
9899
return e.Message
99100
}
100101

101-
type Int32OverflowError struct {
102-
Value int64
102+
type NumberOverflowError struct {
103+
Value any
103104
*IntegerError
104105
}
105106

106-
func newInt32OverflowError(i int64) *Int32OverflowError {
107-
return &Int32OverflowError{
108-
Value: i,
109-
IntegerError: &IntegerError{
110-
Message: fmt.Sprintf("%d overflows signed 32-bit integer", i),
111-
},
107+
type maxNumber interface {
108+
int64 | uint64
109+
}
110+
111+
func newNumberOverflowError[N maxNumber](i any, bitsize int) *NumberOverflowError {
112+
switch v := i.(type) {
113+
case int64:
114+
return &NumberOverflowError{
115+
Value: v,
116+
IntegerError: &IntegerError{
117+
Message: fmt.Sprintf("%d overflows signed %d-bit integer", i, bitsize),
118+
},
119+
}
120+
default:
121+
return &NumberOverflowError{
122+
Value: v,
123+
IntegerError: &IntegerError{
124+
Message: fmt.Sprintf("%d overflows unsigned %d-bit integer", i, bitsize),
125+
},
126+
}
112127
}
113128
}
114129

115-
func (e *Int32OverflowError) Unwrap() error {
130+
func (e *NumberOverflowError) Unwrap() error {
116131
return e.IntegerError
117132
}
118133

119-
func safeCastInt32(i int64) (int32, error) {
120-
if i > math.MaxInt32 || i < math.MinInt32 {
121-
return 0, newInt32OverflowError(i)
134+
// safeCastSignedNumber converts an int64 to a number of type N.
135+
func safeCastSignedNumber[N number](val int64) (N, error) {
136+
var zero N
137+
switch any(zero).(type) {
138+
case int8:
139+
if val > math.MaxInt8 || val < math.MinInt8 {
140+
return 0, newNumberOverflowError[int64](val, 8)
141+
}
142+
case int16:
143+
if val > math.MaxInt16 || val < math.MinInt16 {
144+
return 0, newNumberOverflowError[int64](val, 16)
145+
}
146+
case int32:
147+
if val > math.MaxInt32 || val < math.MinInt32 {
148+
return 0, newNumberOverflowError[int64](val, 32)
149+
}
150+
case int:
151+
if strconv.IntSize == 32 && (val > math.MaxInt32 || val < math.MinInt32) {
152+
return 0, newNumberOverflowError[int64](val, 32)
153+
}
154+
case int64:
155+
default:
156+
return 0, fmt.Errorf("invalid type %T", zero)
122157
}
123-
return int32(i), nil
158+
159+
return N(val), nil
124160
}

0 commit comments

Comments
 (0)