From fbd60bfe6b3c2ccc7d1823396bffcbf850b85216 Mon Sep 17 00:00:00 2001 From: aydinomer00 <109145643+aydinomer00@users.noreply.github.com> Date: Fri, 3 Jan 2025 11:55:56 +0300 Subject: [PATCH] feat(binding): add CustomDecimal type for parsing decimal numbers with leading dot This commit adds support for parsing decimal numbers that start with a dot (e.g. '.1') in query parameters and form data. It implements the BindUnmarshaler interface to handle this special case. Fixes #4089 --- binding/decimal.go | 29 ++++++++++++++++ binding/decimal_test.go | 59 +++++++++++++++++++++++++++++++++ examples/custom-decimal/main.go | 31 +++++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 binding/decimal.go create mode 100644 binding/decimal_test.go create mode 100644 examples/custom-decimal/main.go diff --git a/binding/decimal.go b/binding/decimal.go new file mode 100644 index 0000000000..df838906d8 --- /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 0000000000..6694379eb8 --- /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 0000000000..f6ae449b05 --- /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") +}