mirror of
https://github.com/gin-gonic/gin.git
synced 2026-07-13 06:31:18 +08:00
Compare commits
4 Commits
50026c96bb
...
d8e288e4d0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8e288e4d0 | ||
|
|
2a794cd0b0 | ||
|
|
b917b14ff9 | ||
|
|
fbd60bfe6b |
29
binding/decimal.go
Normal file
29
binding/decimal.go
Normal file
@ -0,0 +1,29 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"github.com/shopspring/decimal"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CustomDecimal represents a decimal number that can be bound from form values.
|
||||
// It supports values with leading dots (e.g. ".1" is parsed as "0.1").
|
||||
type CustomDecimal struct {
|
||||
decimal.Decimal
|
||||
}
|
||||
|
||||
// UnmarshalParam implements the binding.BindUnmarshaler interface.
|
||||
// It converts form values to decimal.Decimal, with special handling for
|
||||
// values that start with a dot (e.g. ".1" becomes "0.1").
|
||||
func (cd *CustomDecimal) UnmarshalParam(val string) error {
|
||||
if strings.HasPrefix(val, ".") {
|
||||
val = "0" + val
|
||||
}
|
||||
|
||||
dec, err := decimal.NewFromString(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cd.Decimal = dec
|
||||
return nil
|
||||
}
|
||||
59
binding/decimal_test.go
Normal file
59
binding/decimal_test.go
Normal file
@ -0,0 +1,59 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCustomDecimalUnmarshalParam(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "leading dot",
|
||||
input: ".1",
|
||||
want: "0.1",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid decimal",
|
||||
input: "abc",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "leading dot with multiple digits",
|
||||
input: ".123",
|
||||
want: "0.123",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "normal decimal",
|
||||
input: "1.23",
|
||||
want: "1.23",
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var cd CustomDecimal
|
||||
err := cd.UnmarshalParam(tt.input)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.want, cd.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -300,6 +300,11 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
|
||||
}
|
||||
|
||||
func setWithProperType(val string, value reflect.Value, field reflect.StructField) error {
|
||||
// If it is a string type, no spaces are removed, and the user data is not modified here
|
||||
if value.Kind() != reflect.String {
|
||||
val = strings.TrimSpace(val)
|
||||
}
|
||||
|
||||
switch value.Kind() {
|
||||
case reflect.Int:
|
||||
return setIntField(val, 0, value)
|
||||
@ -404,6 +409,11 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
|
||||
timeFormat = time.RFC3339
|
||||
}
|
||||
|
||||
if val == "" {
|
||||
value.Set(reflect.ValueOf(time.Time{}))
|
||||
return nil
|
||||
}
|
||||
|
||||
switch tf := strings.ToLower(timeFormat); tf {
|
||||
case "unix", "unixmilli", "unixmicro", "unixnano":
|
||||
tv, err := strconv.ParseInt(val, 10, 64)
|
||||
@ -427,11 +437,6 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
|
||||
return nil
|
||||
}
|
||||
|
||||
if val == "" {
|
||||
value.Set(reflect.ValueOf(time.Time{}))
|
||||
return nil
|
||||
}
|
||||
|
||||
l := time.Local
|
||||
if isUTC, _ := strconv.ParseBool(structField.Tag.Get("time_utc")); isUTC {
|
||||
l = time.UTC
|
||||
@ -475,6 +480,10 @@ func setSlice(vals []string, value reflect.Value, field reflect.StructField) err
|
||||
}
|
||||
|
||||
func setTimeDuration(val string, value reflect.Value) error {
|
||||
if val == "" {
|
||||
val = "0"
|
||||
}
|
||||
|
||||
d, err := time.ParseDuration(val)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@ -226,7 +226,35 @@ func TestMappingTime(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
type bindTestData struct {
|
||||
need any
|
||||
got any
|
||||
in map[string][]string
|
||||
}
|
||||
|
||||
func TestMappingTimeUnixNano(t *testing.T) {
|
||||
type needFixUnixNanoEmpty struct {
|
||||
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
|
||||
}
|
||||
|
||||
// ok
|
||||
tests := []bindTestData{
|
||||
{need: &needFixUnixNanoEmpty{}, got: &needFixUnixNanoEmpty{}, in: formSource{"createTime": []string{" "}}},
|
||||
{need: &needFixUnixNanoEmpty{}, got: &needFixUnixNanoEmpty{}, in: formSource{"createTime": []string{}}},
|
||||
}
|
||||
|
||||
for _, v := range tests {
|
||||
err := mapForm(v.got, v.in)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, v.need, v.got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMappingTimeDuration(t *testing.T) {
|
||||
type needFixDurationEmpty struct {
|
||||
Duration time.Duration `form:"duration"`
|
||||
}
|
||||
|
||||
var s struct {
|
||||
D time.Duration
|
||||
}
|
||||
@ -236,6 +264,17 @@ func TestMappingTimeDuration(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 5*time.Second, s.D)
|
||||
|
||||
// ok
|
||||
tests := []bindTestData{
|
||||
{need: &needFixDurationEmpty{}, got: &needFixDurationEmpty{}, in: formSource{"duration": []string{" "}}},
|
||||
{need: &needFixDurationEmpty{}, got: &needFixDurationEmpty{}, in: formSource{"duration": []string{}}},
|
||||
}
|
||||
|
||||
for _, v := range tests {
|
||||
err := mapForm(v.got, v.in)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, v.need, v.got)
|
||||
}
|
||||
// error
|
||||
err = mappingByPtr(&s, formSource{"D": {"wrong"}}, "form")
|
||||
require.Error(t, err)
|
||||
|
||||
2
debug.go
2
debug.go
@ -13,7 +13,7 @@ import (
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
const ginSupportMinGoVer = 23
|
||||
const ginSupportMinGoVer = 24
|
||||
|
||||
// IsDebugging returns true if the framework is running in debug mode.
|
||||
// Use SetMode(gin.ReleaseMode) to disable debug mode.
|
||||
|
||||
31
examples/custom-decimal/main.go
Normal file
31
examples/custom-decimal/main.go
Normal file
@ -0,0 +1,31 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type QueryParams struct {
|
||||
Amount binding.CustomDecimal `form:"amount"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
|
||||
r.GET("/amount", func(c *gin.Context) {
|
||||
var params QueryParams
|
||||
if err := c.BindQuery(¶ms); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"amount": params.Amount.String(),
|
||||
})
|
||||
})
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user