Skip to content

Commit

Permalink
Path param int deserialization (#381)
Browse files Browse the repository at this point in the history
* Add ContextWithBody signatures

* Implement for netHttpContext

* Fix go.work

* Implement for echoContext and ginContext

* Implement for MockContext

* Refactor errors

* Add tests

* Refactor logic

* Format

* Adds test and status code to PathParamIntErr

* Moved comment to above the field

---------

Co-authored-by: EwenQuim <[email protected]>
  • Loading branch information
thezbm and EwenQuim authored Feb 3, 2025
1 parent a6d1c8e commit 0ba4092
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 0 deletions.
69 changes: 69 additions & 0 deletions ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"io/fs"
"net/http"
"net/url"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -44,6 +45,9 @@ type ContextWithBody[B any] interface {
// ...
// })
PathParam(name string) string
// If the path parameter is not provided or is not an int, it returns 0. Use [Ctx.PathParamIntErr] if you want to know if the path parameter is erroneous.
PathParamInt(name string) int
PathParamIntErr(name string) (int, error)

QueryParam(name string) string
QueryParamArr(name string) []string
Expand Down Expand Up @@ -220,6 +224,71 @@ func (c netHttpContext[B]) PathParam(name string) string {
return c.Req.PathValue(name)
}

type PathParamNotFoundError struct {
ParamName string
}

func (e PathParamNotFoundError) Error() string {
return fmt.Errorf("param %s not found", e.ParamName).Error()
}

func (e PathParamNotFoundError) StatusCode() int { return 404 }

type PathParamInvalidTypeError struct {
Err error
ParamName string
ParamValue string
ExpectedType string
}

func (e PathParamInvalidTypeError) Error() string {
return fmt.Errorf("param %s=%s is not of type %s: %w", e.ParamName, e.ParamValue, e.ExpectedType, e.Err).Error()
}

func (e PathParamInvalidTypeError) StatusCode() int { return 422 }

type ContextWithPathParam interface {
PathParam(name string) string
}

func PathParamIntErr(c ContextWithPathParam, name string) (int, error) {
param := c.PathParam(name)
if param == "" {
return 0, PathParamNotFoundError{ParamName: name}
}

i, err := strconv.Atoi(param)
if err != nil {
return 0, PathParamInvalidTypeError{
ParamName: name,
ParamValue: param,
ExpectedType: "int",
Err: err,
}
}

return i, nil
}

func (c netHttpContext[B]) PathParamIntErr(name string) (int, error) {
return PathParamIntErr(c, name)
}

func PathParamInt(c ContextWithPathParam, name string) int {
param, err := PathParamIntErr(c, name)
if err != nil {
return 0
}

return param
}

// PathParamInt returns the path parameter with the given name as an int.
// If the query parameter does not exist, or if it is not an int, it returns 0.
func (c netHttpContext[B]) PathParamInt(name string) int {
return PathParamInt(c, name)
}

func (c netHttpContext[B]) MainLang() string {
return strings.Split(c.MainLocale(), "-")[0]
}
Expand Down
61 changes: 61 additions & 0 deletions ctx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"encoding/xml"
"errors"
"fmt"
"net/http/httptest"
"strings"
"testing"
Expand All @@ -27,6 +28,66 @@ func TestContext_PathParam(t *testing.T) {
require.Equal(t, crlf(`{"ans":"123"}`), w.Body.String())
})

t.Run("can read one path param to int", func(t *testing.T) {
s := NewServer()
Get(s, "/foo/{id}", func(c ContextNoBody) (ans, error) {
return ans{Ans: fmt.Sprintf("%d", c.PathParamInt("id"))}, nil
})

r := httptest.NewRequest("GET", "/foo/123", nil)
w := httptest.NewRecorder()

s.Mux.ServeHTTP(w, r)

require.Equal(t, crlf(`{"ans":"123"}`), w.Body.String())
})

t.Run("reading non-int path param to int defaults to 0", func(t *testing.T) {
s := NewServer()
Get(s, "/foo/{id}", func(c ContextNoBody) (ans, error) {
return ans{Ans: fmt.Sprintf("%d", c.PathParamInt("id"))}, nil
})

r := httptest.NewRequest("GET", "/foo/abc", nil)
w := httptest.NewRecorder()

s.Mux.ServeHTTP(w, r)

require.Equal(t, crlf(`{"ans":"0"}`), w.Body.String())
})

t.Run("reading missing path param to int defaults to 0", func(t *testing.T) {
s := NewServer()
Get(s, "/foo/", func(c ContextNoBody) (ans, error) {
return ans{Ans: fmt.Sprintf("%d", c.PathParamInt("id"))}, nil
})

r := httptest.NewRequest("GET", "/foo/", nil)
w := httptest.NewRecorder()

s.Mux.ServeHTTP(w, r)

require.Equal(t, crlf(`{"ans":"0"}`), w.Body.String())
})

t.Run("reading non-int path param to int sends an error", func(t *testing.T) {
s := NewServer()
Get(s, "/foo/{id}", func(c ContextNoBody) (ans, error) {
id, err := c.PathParamIntErr("id")
if err != nil {
return ans{}, err
}
return ans{Ans: fmt.Sprintf("%d", id)}, nil
})

r := httptest.NewRequest("GET", "/foo/abc", nil)
w := httptest.NewRecorder()

s.Mux.ServeHTTP(w, r)

require.Equal(t, 422, w.Code)
})

t.Run("path param invalid", func(t *testing.T) {
s := NewServer()
Get(s, "/foo/", func(c ContextNoBody) (ans, error) {
Expand Down
8 changes: 8 additions & 0 deletions extra/fuegoecho/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ func (c echoContext[B]) PathParam(name string) string {
return c.echoCtx.Param(name)
}

func (c echoContext[B]) PathParamIntErr(name string) (int, error) {
return fuego.PathParamIntErr(c, name)
}

func (c echoContext[B]) PathParamInt(name string) int {
return fuego.PathParamInt(c, name)
}

func (c echoContext[B]) MainLang() string {
return strings.Split(c.MainLocale(), "-")[0]
}
Expand Down
8 changes: 8 additions & 0 deletions extra/fuegogin/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ func (c ginContext[B]) PathParam(name string) string {
return c.ginCtx.Param(name)
}

func (c ginContext[B]) PathParamIntErr(name string) (int, error) {
return fuego.PathParamIntErr(c, name)
}

func (c ginContext[B]) PathParamInt(name string) int {
return fuego.PathParamInt(c, name)
}

func (c ginContext[B]) MainLang() string {
return strings.Split(c.MainLocale(), "-")[0]
}
Expand Down
1 change: 1 addition & 0 deletions go.work
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use (
./examples/petstore
./examples/with-listener
./extra/fuegogin
./extra/fuegoecho
./extra/markdown
./middleware/basicauth
./middleware/cache
Expand Down
12 changes: 12 additions & 0 deletions mock_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"

"github.com/go-fuego/fuego/internal"
Expand Down Expand Up @@ -84,6 +85,17 @@ func (m *MockContext[B]) PathParam(name string) string {
return m.PathParams[name]
}

func (m *MockContext[B]) PathParamIntErr(name string) (int, error) {
return strconv.Atoi(m.PathParams[name])
}

func (m *MockContext[B]) PathParamInt(name string) int {
if i, err := m.PathParamIntErr(name); err == nil {
return i
}
return 0
}

// Request returns the mock request
func (m *MockContext[B]) Request() *http.Request {
return m.request
Expand Down

0 comments on commit 0ba4092

Please sign in to comment.