Skip to content

Commit

Permalink
feat(binding): add CustomDecimal type for parsing decimal numbers wit…
Browse files Browse the repository at this point in the history
…h 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
  • Loading branch information
aydinomer00 committed Jan 3, 2025
1 parent 3f818c3 commit fbd60bf
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 0 deletions.
29 changes: 29 additions & 0 deletions binding/decimal.go
Original file line number Diff line number Diff line change
@@ -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 changes: 59 additions & 0 deletions binding/decimal_test.go
Original file line number Diff line number Diff line change
@@ -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())
})
}
}
31 changes: 31 additions & 0 deletions examples/custom-decimal/main.go
Original file line number Diff line number Diff line change
@@ -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(&params); 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")
}

0 comments on commit fbd60bf

Please sign in to comment.