diff --git a/binding/decimal.go b/binding/decimal.go new file mode 100644 index 00000000..df838906 --- /dev/null +++ b/binding/decimal.go @@ -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 +} diff --git a/binding/decimal_test.go b/binding/decimal_test.go new file mode 100644 index 00000000..6694379e --- /dev/null +++ b/binding/decimal_test.go @@ -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()) + }) + } +} diff --git a/examples/custom-decimal/main.go b/examples/custom-decimal/main.go new file mode 100644 index 00000000..f6ae449b --- /dev/null +++ b/examples/custom-decimal/main.go @@ -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") +}