Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Path param int deserialization #381

Merged
merged 11 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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{
dylanhitt marked this conversation as resolved.
Show resolved Hide resolved
ParamName: name,
ParamValue: param,
ExpectedType: "int",
Err: err,
}
}

return i, nil
}
EwenQuim marked this conversation as resolved.
Show resolved Hide resolved

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())
})
EwenQuim marked this conversation as resolved.
Show resolved Hide resolved

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