From 060aec444af17c2fb7450b126265dab211c1c239 Mon Sep 17 00:00:00 2001 From: cxlblm Date: Mon, 24 Aug 2026 14:01:35 +0800 Subject: [PATCH 1/2] feat: add Go 1.27 generic parsing methods to Context --- context_generic_params.go | 76 +++++++++++++++++ context_generic_params_test.go | 151 +++++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 context_generic_params.go create mode 100644 context_generic_params_test.go 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..c0c315348 --- /dev/null +++ b/context_generic_params_test.go @@ -0,0 +1,151 @@ +// 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"}, + }) + + value, err := c.ParsePathParam[int]("id") + assert.NoError(t, err) + assert.Equal(t, 42, value) + + value, err = c.ParsePathParam[int]("missing") + assert.ErrorIs(t, err, ErrNonExistentKey) + assert.Zero(t, value) + + value, err = c.ParsePathParam[int]("invalid") + assert.ErrorContains(t, err, "message=path value") + assert.Zero(t, value) + + value, err = c.ParsePathParamOr[int]("missing", 99) + assert.NoError(t, err) + assert.Equal(t, 99, value) + + value, err = c.ParsePathParamOr[int]("empty", 99) + assert.NoError(t, err) + assert.Equal(t, 99, value) +} + +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) + + value, err := c.ParseQueryParam[int]("page") + assert.NoError(t, err) + assert.Equal(t, 42, value) + + value, err = c.ParseQueryParam[int]("missing") + assert.ErrorIs(t, err, ErrNonExistentKey) + assert.Zero(t, value) + + value, err = c.ParseQueryParam[int]("invalid") + assert.ErrorContains(t, err, "message=query param") + assert.Zero(t, value) + + value, err = c.ParseQueryParamOr[int]("missing", 99) + assert.NoError(t, err) + assert.Equal(t, 99, value) + + value, err = c.ParseQueryParamOr[int]("empty", 99) + assert.NoError(t, err) + assert.Equal(t, 99, value) + + date, err := c.ParseQueryParam[time.Time]("date", TimeLayout(time.DateOnly)) + assert.NoError(t, err) + assert.Equal(t, time.Date(2026, time.August, 24, 0, 0, 0, 0, time.UTC), date) +} + +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) + + values, err := c.ParseQueryParams[int]("id") + assert.NoError(t, err) + assert.Equal(t, []int{1, 2, 3}, values) + + values, err = c.ParseQueryParams[int]("missing") + assert.ErrorIs(t, err, ErrNonExistentKey) + assert.Nil(t, values) + + values, err = c.ParseQueryParams[int]("invalid") + assert.ErrorContains(t, err, "message=query params") + assert.Nil(t, values) + + values, err = c.ParseQueryParamsOr[int]("missing", []int{98, 99}) + assert.NoError(t, err) + assert.Equal(t, []int{98, 99}, values) +} + +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) + + value, err := c.ParseFormValue[int]("count") + assert.NoError(t, err) + assert.Equal(t, 42, value) + + value, err = c.ParseFormValue[int]("missing") + assert.ErrorIs(t, err, ErrNonExistentKey) + assert.Zero(t, value) + + value, err = c.ParseFormValue[int]("invalid") + assert.ErrorContains(t, err, "message=form value") + assert.Zero(t, value) + + value, err = c.ParseFormValueOr[int]("missing", 99) + assert.NoError(t, err) + assert.Equal(t, 99, value) + + value, err = c.ParseFormValueOr[int]("empty", 99) + assert.NoError(t, err) + assert.Equal(t, 99, value) +} + +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) + + values, err := c.ParseFormValues[int]("id") + assert.NoError(t, err) + assert.Equal(t, []int{1, 2, 3}, values) + + values, err = c.ParseFormValues[int]("missing") + assert.ErrorIs(t, err, ErrNonExistentKey) + assert.Nil(t, values) + + values, err = c.ParseFormValues[int]("invalid") + assert.ErrorContains(t, err, "message=form values") + assert.Nil(t, values) + + values, err = c.ParseFormValuesOr[int]("missing", []int{98, 99}) + assert.NoError(t, err) + assert.Equal(t, []int{98, 99}, values) +} From ae64791e9a961132e3facbb56f13eb212e6e1414 Mon Sep 17 00:00:00 2001 From: cxlblm Date: Mon, 24 Aug 2026 15:25:55 +0800 Subject: [PATCH 2/2] test: use table-driven context parsing tests --- context_generic_params_test.go | 336 ++++++++++++++++++++++++--------- 1 file changed, 245 insertions(+), 91 deletions(-) diff --git a/context_generic_params_test.go b/context_generic_params_test.go index c0c315348..fa09b7284 100644 --- a/context_generic_params_test.go +++ b/context_generic_params_test.go @@ -23,75 +23,168 @@ func TestContextParsePathParam(t *testing.T) { {Name: "invalid", Value: "not-an-int"}, }) - value, err := c.ParsePathParam[int]("id") - assert.NoError(t, err) - assert.Equal(t, 42, value) - - value, err = c.ParsePathParam[int]("missing") - assert.ErrorIs(t, err, ErrNonExistentKey) - assert.Zero(t, value) - - value, err = c.ParsePathParam[int]("invalid") - assert.ErrorContains(t, err, "message=path value") - assert.Zero(t, value) - - value, err = c.ParsePathParamOr[int]("missing", 99) - assert.NoError(t, err) - assert.Equal(t, 99, value) - - value, err = c.ParsePathParamOr[int]("empty", 99) - assert.NoError(t, err) - assert.Equal(t, 99, value) + 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) - value, err := c.ParseQueryParam[int]("page") - assert.NoError(t, err) - assert.Equal(t, 42, value) - - value, err = c.ParseQueryParam[int]("missing") - assert.ErrorIs(t, err, ErrNonExistentKey) - assert.Zero(t, value) - - value, err = c.ParseQueryParam[int]("invalid") - assert.ErrorContains(t, err, "message=query param") - assert.Zero(t, value) - - value, err = c.ParseQueryParamOr[int]("missing", 99) - assert.NoError(t, err) - assert.Equal(t, 99, value) - - value, err = c.ParseQueryParamOr[int]("empty", 99) - assert.NoError(t, err) - assert.Equal(t, 99, value) - - date, err := c.ParseQueryParam[time.Time]("date", TimeLayout(time.DateOnly)) - assert.NoError(t, err) - assert.Equal(t, time.Date(2026, time.August, 24, 0, 0, 0, 0, time.UTC), date) + 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) - values, err := c.ParseQueryParams[int]("id") - assert.NoError(t, err) - assert.Equal(t, []int{1, 2, 3}, values) - - values, err = c.ParseQueryParams[int]("missing") - assert.ErrorIs(t, err, ErrNonExistentKey) - assert.Nil(t, values) - - values, err = c.ParseQueryParams[int]("invalid") - assert.ErrorContains(t, err, "message=query params") - assert.Nil(t, values) - - values, err = c.ParseQueryParamsOr[int]("missing", []int{98, 99}) - assert.NoError(t, err) - assert.Equal(t, []int{98, 99}, values) + 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) { @@ -103,25 +196,56 @@ func TestContextParseFormValue(t *testing.T) { req.Header.Set(HeaderContentType, MIMEApplicationForm) c := NewContext(req, nil) - value, err := c.ParseFormValue[int]("count") - assert.NoError(t, err) - assert.Equal(t, 42, value) - - value, err = c.ParseFormValue[int]("missing") - assert.ErrorIs(t, err, ErrNonExistentKey) - assert.Zero(t, value) - - value, err = c.ParseFormValue[int]("invalid") - assert.ErrorContains(t, err, "message=form value") - assert.Zero(t, value) - - value, err = c.ParseFormValueOr[int]("missing", 99) - assert.NoError(t, err) - assert.Equal(t, 99, value) - - value, err = c.ParseFormValueOr[int]("empty", 99) - assert.NoError(t, err) - assert.Equal(t, 99, value) + 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) { @@ -133,19 +257,49 @@ func TestContextParseFormValues(t *testing.T) { req.Header.Set(HeaderContentType, MIMEApplicationForm) c := NewContext(req, nil) - values, err := c.ParseFormValues[int]("id") - assert.NoError(t, err) - assert.Equal(t, []int{1, 2, 3}, values) - - values, err = c.ParseFormValues[int]("missing") - assert.ErrorIs(t, err, ErrNonExistentKey) - assert.Nil(t, values) - - values, err = c.ParseFormValues[int]("invalid") - assert.ErrorContains(t, err, "message=form values") - assert.Nil(t, values) - - values, err = c.ParseFormValuesOr[int]("missing", []int{98, 99}) - assert.NoError(t, err) - assert.Equal(t, []int{98, 99}, values) + 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) + }) + } }