diff --git a/context_generic_params.go b/context_generic_params.go new file mode 100644 index 000000000..6bf2d4f7e --- /dev/null +++ b/context_generic_params.go @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors + +//go:build go1.27 + +package echo + +// ParsePathParam returns the path parameter by name parsed as type T. +// It returns ErrNonExistentKey if the parameter does not exist. +// See PathParam for supported types, options, and parsing behavior. +func (c *Context) ParsePathParam[T any](paramName string, opts ...any) (T, error) { + return PathParam[T](c, paramName, opts...) +} + +// ParsePathParamOr returns the path parameter by name parsed as type T, or defaultValue if the +// parameter does not exist or is empty. +// See PathParamOr for supported types, options, and parsing behavior. +func (c *Context) ParsePathParamOr[T any](paramName string, defaultValue T, opts ...any) (T, error) { + return PathParamOr[T](c, paramName, defaultValue, opts...) +} + +// ParseQueryParam returns the first query parameter value for key parsed as type T. +// It returns ErrNonExistentKey if the parameter does not exist. +// See QueryParam for supported types, options, and parsing behavior. +func (c *Context) ParseQueryParam[T any](key string, opts ...any) (T, error) { + return QueryParam[T](c, key, opts...) +} + +// ParseQueryParamOr returns the first query parameter value for key parsed as type T, or +// defaultValue if the parameter does not exist or is empty. +// See QueryParamOr for supported types, options, and parsing behavior. +func (c *Context) ParseQueryParamOr[T any](key string, defaultValue T, opts ...any) (T, error) { + return QueryParamOr[T](c, key, defaultValue, opts...) +} + +// ParseQueryParams returns all query parameter values for key parsed as a slice of T. +// It returns ErrNonExistentKey if the parameter does not exist. +// See QueryParams for supported types, options, and parsing behavior. +func (c *Context) ParseQueryParams[T any](key string, opts ...any) ([]T, error) { + return QueryParams[T](c, key, opts...) +} + +// ParseQueryParamsOr returns all query parameter values for key parsed as a slice of T, or +// defaultValue if the parameter does not exist. +// See QueryParamsOr for supported types, options, and parsing behavior. +func (c *Context) ParseQueryParamsOr[T any](key string, defaultValue []T, opts ...any) ([]T, error) { + return QueryParamsOr[T](c, key, defaultValue, opts...) +} + +// ParseFormValue returns the first form field value for key parsed as type T. +// It returns ErrNonExistentKey if the field does not exist. +// See FormValue for supported types, options, and parsing behavior. +func (c *Context) ParseFormValue[T any](key string, opts ...any) (T, error) { + return FormValue[T](c, key, opts...) +} + +// ParseFormValueOr returns the first form field value for key parsed as type T, or defaultValue if +// the field does not exist or is empty. +// See FormValueOr for supported types, options, and parsing behavior. +func (c *Context) ParseFormValueOr[T any](key string, defaultValue T, opts ...any) (T, error) { + return FormValueOr[T](c, key, defaultValue, opts...) +} + +// ParseFormValues returns all form field values for key parsed as a slice of T. +// It returns ErrNonExistentKey if the field does not exist. +// See FormValues for supported types, options, and parsing behavior. +func (c *Context) ParseFormValues[T any](key string, opts ...any) ([]T, error) { + return FormValues[T](c, key, opts...) +} + +// ParseFormValuesOr returns all form field values for key parsed as a slice of T, or defaultValue if +// the field does not exist. +// See FormValuesOr for supported types, options, and parsing behavior. +func (c *Context) ParseFormValuesOr[T any](key string, defaultValue []T, opts ...any) ([]T, error) { + return FormValuesOr[T](c, key, defaultValue, opts...) +} diff --git a/context_generic_params_test.go b/context_generic_params_test.go new file mode 100644 index 000000000..fa09b7284 --- /dev/null +++ b/context_generic_params_test.go @@ -0,0 +1,305 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors + +//go:build go1.27 + +package echo + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestContextParsePathParam(t *testing.T) { + c := NewContext(nil, nil) + c.SetPathValues(PathValues{ + {Name: "id", Value: "42"}, + {Name: "empty", Value: ""}, + {Name: "invalid", Value: "not-an-int"}, + }) + + var testCases = []struct { + name string + parse func() (any, error) + want any + wantErr error + wantErrContains string + }{ + { + name: "value", + parse: func() (any, error) { return c.ParsePathParam[int]("id") }, + want: 42, + }, + { + name: "missing", + parse: func() (any, error) { return c.ParsePathParam[int]("missing") }, + want: 0, + wantErr: ErrNonExistentKey, + }, + { + name: "invalid", + parse: func() (any, error) { return c.ParsePathParam[int]("invalid") }, + want: 0, + wantErrContains: "message=path value", + }, + { + name: "missing with default", + parse: func() (any, error) { return c.ParsePathParamOr[int]("missing", 99) }, + want: 99, + }, + { + name: "empty with default", + parse: func() (any, error) { return c.ParsePathParamOr[int]("empty", 99) }, + want: 99, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.parse() + switch { + case tc.wantErr != nil: + assert.ErrorIs(t, err, tc.wantErr) + case tc.wantErrContains != "": + assert.ErrorContains(t, err, tc.wantErrContains) + default: + assert.NoError(t, err) + } + assert.Equal(t, tc.want, got) + }) + } +} + +func TestContextParseQueryParam(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/?page=42&empty=&invalid=not-an-int&date=2026-08-24", nil) + c := NewContext(req, nil) + + var testCases = []struct { + name string + parse func() (any, error) + want any + wantErr error + wantErrContains string + }{ + { + name: "value", + parse: func() (any, error) { return c.ParseQueryParam[int]("page") }, + want: 42, + }, + { + name: "missing", + parse: func() (any, error) { return c.ParseQueryParam[int]("missing") }, + want: 0, + wantErr: ErrNonExistentKey, + }, + { + name: "invalid", + parse: func() (any, error) { return c.ParseQueryParam[int]("invalid") }, + want: 0, + wantErrContains: "message=query param", + }, + { + name: "missing with default", + parse: func() (any, error) { return c.ParseQueryParamOr[int]("missing", 99) }, + want: 99, + }, + { + name: "empty with default", + parse: func() (any, error) { return c.ParseQueryParamOr[int]("empty", 99) }, + want: 99, + }, + { + name: "time layout option", + parse: func() (any, error) { return c.ParseQueryParam[time.Time]("date", TimeLayout(time.DateOnly)) }, + want: time.Date(2026, time.August, 24, 0, 0, 0, 0, time.UTC), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.parse() + switch { + case tc.wantErr != nil: + assert.ErrorIs(t, err, tc.wantErr) + case tc.wantErrContains != "": + assert.ErrorContains(t, err, tc.wantErrContains) + default: + assert.NoError(t, err) + } + assert.Equal(t, tc.want, got) + }) + } +} + +func TestContextParseQueryParams(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/?id=1&id=2&id=3&invalid=1&invalid=not-an-int", nil) + c := NewContext(req, nil) + + var testCases = []struct { + name string + parse func() (any, error) + want any + wantErr error + wantErrContains string + }{ + { + name: "values", + parse: func() (any, error) { return c.ParseQueryParams[int]("id") }, + want: []int{1, 2, 3}, + }, + { + name: "missing", + parse: func() (any, error) { return c.ParseQueryParams[int]("missing") }, + want: []int(nil), + wantErr: ErrNonExistentKey, + }, + { + name: "invalid", + parse: func() (any, error) { return c.ParseQueryParams[int]("invalid") }, + want: []int(nil), + wantErrContains: "message=query params", + }, + { + name: "missing with default", + parse: func() (any, error) { return c.ParseQueryParamsOr[int]("missing", []int{98, 99}) }, + want: []int{98, 99}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.parse() + switch { + case tc.wantErr != nil: + assert.ErrorIs(t, err, tc.wantErr) + case tc.wantErrContains != "": + assert.ErrorContains(t, err, tc.wantErrContains) + default: + assert.NoError(t, err) + } + assert.Equal(t, tc.want, got) + }) + } +} + +func TestContextParseFormValue(t *testing.T) { + req := httptest.NewRequest( + http.MethodPost, + "/", + strings.NewReader("count=42&empty=&invalid=not-an-int"), + ) + req.Header.Set(HeaderContentType, MIMEApplicationForm) + c := NewContext(req, nil) + + var testCases = []struct { + name string + parse func() (any, error) + want any + wantErr error + wantErrContains string + }{ + { + name: "value", + parse: func() (any, error) { return c.ParseFormValue[int]("count") }, + want: 42, + }, + { + name: "missing", + parse: func() (any, error) { return c.ParseFormValue[int]("missing") }, + want: 0, + wantErr: ErrNonExistentKey, + }, + { + name: "invalid", + parse: func() (any, error) { return c.ParseFormValue[int]("invalid") }, + want: 0, + wantErrContains: "message=form value", + }, + { + name: "missing with default", + parse: func() (any, error) { return c.ParseFormValueOr[int]("missing", 99) }, + want: 99, + }, + { + name: "empty with default", + parse: func() (any, error) { return c.ParseFormValueOr[int]("empty", 99) }, + want: 99, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.parse() + switch { + case tc.wantErr != nil: + assert.ErrorIs(t, err, tc.wantErr) + case tc.wantErrContains != "": + assert.ErrorContains(t, err, tc.wantErrContains) + default: + assert.NoError(t, err) + } + assert.Equal(t, tc.want, got) + }) + } +} + +func TestContextParseFormValues(t *testing.T) { + req := httptest.NewRequest( + http.MethodPost, + "/", + strings.NewReader("id=1&id=2&id=3&invalid=1&invalid=not-an-int"), + ) + req.Header.Set(HeaderContentType, MIMEApplicationForm) + c := NewContext(req, nil) + + var testCases = []struct { + name string + parse func() (any, error) + want any + wantErr error + wantErrContains string + }{ + { + name: "values", + parse: func() (any, error) { return c.ParseFormValues[int]("id") }, + want: []int{1, 2, 3}, + }, + { + name: "missing", + parse: func() (any, error) { return c.ParseFormValues[int]("missing") }, + want: []int(nil), + wantErr: ErrNonExistentKey, + }, + { + name: "invalid", + parse: func() (any, error) { return c.ParseFormValues[int]("invalid") }, + want: []int(nil), + wantErrContains: "message=form values", + }, + { + name: "missing with default", + parse: func() (any, error) { return c.ParseFormValuesOr[int]("missing", []int{98, 99}) }, + want: []int{98, 99}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.parse() + switch { + case tc.wantErr != nil: + assert.ErrorIs(t, err, tc.wantErr) + case tc.wantErrContains != "": + assert.ErrorContains(t, err, tc.wantErrContains) + default: + assert.NoError(t, err) + } + assert.Equal(t, tc.want, got) + }) + } +}