Skip to content

Commit 8fd0e18

Browse files
shblue21vishr
andauthored
feat(context): add ValidateCtx (#3152)
* feat: pass request context to context-aware validators * feat: make context-aware validation explicit * test(context): cover request replacement in ValidateCtx Document context-aware validation precedence, embedded wrapper responsibilities, and cancellation handling. --------- Co-authored-by: Vishal Rana <v@labstack.com>
1 parent 3882266 commit 8fd0e18

2 files changed

Lines changed: 156 additions & 1 deletion

File tree

‎context.go‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package echo
55

66
import (
77
"bytes"
8+
stdContext "context"
89
"encoding/xml"
910
"errors"
1011
"fmt"
@@ -460,6 +461,10 @@ func (c *Context) Bind(i any) error {
460461
return c.echo.Binder.Bind(c, i)
461462
}
462463

464+
type validatorCtx interface {
465+
ValidateCtx(ctx stdContext.Context, i any) error
466+
}
467+
463468
// Validate validates provided `i`. It is usually called after `Context#Bind()`.
464469
// Validator must be registered using `Echo#Validator`.
465470
func (c *Context) Validate(i any) error {
@@ -469,6 +474,27 @@ func (c *Context) Validate(i any) error {
469474
return c.echo.Validator.Validate(i)
470475
}
471476

477+
// ValidateCtx validates i using the current request's context. If the registered
478+
// Validator implements ValidateCtx(context.Context, any) error, that method is
479+
// called instead of Validate. Otherwise, Validate is called.
480+
//
481+
// The validator must still implement Validate to satisfy the Validator interface.
482+
// Wrappers that embed a context-aware validator must implement ValidateCtx if they
483+
// need to preserve checks performed by their own Validate method.
484+
//
485+
// If there is no request, context.Background() is used. Cancellation and deadlines
486+
// are passed to the validator; the validator is responsible for honoring them.
487+
func (c *Context) ValidateCtx(i any) error {
488+
if v, ok := c.echo.Validator.(validatorCtx); ok {
489+
ctx := stdContext.Background()
490+
if req := c.Request(); req != nil {
491+
ctx = req.Context()
492+
}
493+
return v.ValidateCtx(ctx, i)
494+
}
495+
return c.Validate(i)
496+
}
497+
472498
// Render renders a template with data and sends a text/html response with status
473499
// code. Renderer must be registered using `Echo.Renderer`.
474500
func (c *Context) Render(code int, name string, data any) (err error) {

‎context_test.go‎

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ package echo
55

66
import (
77
"bytes"
8+
stdContext "context"
89
"crypto/tls"
910
"encoding/json"
1011
"encoding/xml"
12+
"errors"
1113
"fmt"
1214
"io"
1315
"io/fs"
@@ -1119,10 +1121,137 @@ func TestContext_Validate(t *testing.T) {
11191121
e := New()
11201122
c := e.NewContext(nil, nil)
11211123

1122-
assert.Error(t, c.Validate(struct{}{}))
1124+
assert.ErrorIs(t, c.Validate(struct{}{}), ErrValidatorNotRegistered)
1125+
assert.ErrorIs(t, c.ValidateCtx(struct{}{}), ErrValidatorNotRegistered)
11231126

11241127
e.Validator = &validator{}
11251128
assert.NoError(t, c.Validate(struct{}{}))
1129+
assert.NoError(t, c.ValidateCtx(struct{}{}))
1130+
}
1131+
1132+
type validationFunc func(any) error
1133+
1134+
func (v validationFunc) Validate(i any) error { return v(i) }
1135+
1136+
type contextValidator struct {
1137+
validationFunc
1138+
validateCtx func(stdContext.Context, any) error
1139+
}
1140+
1141+
func (v contextValidator) ValidateCtx(ctx stdContext.Context, i any) error {
1142+
return v.validateCtx(ctx, i)
1143+
}
1144+
1145+
func TestContext_Validate_legacyError(t *testing.T) {
1146+
e := New()
1147+
c := e.NewContext(nil, nil)
1148+
payload := &struct{ Name string }{Name: "Jon Snow"}
1149+
wantErr := errors.New("validation failed")
1150+
e.Validator = validationFunc(func(i any) error {
1151+
assert.Same(t, payload, i)
1152+
return wantErr
1153+
})
1154+
assert.Same(t, wantErr, c.Validate(payload))
1155+
assert.Same(t, wantErr, c.ValidateCtx(payload))
1156+
}
1157+
1158+
type validationWrapper struct {
1159+
contextValidator
1160+
err error
1161+
}
1162+
1163+
func (v validationWrapper) Validate(any) error { return v.err }
1164+
1165+
func TestContext_Validate_wrapper(t *testing.T) {
1166+
e := New()
1167+
c := e.NewContext(nil, nil)
1168+
wantErr := errors.New("wrapper validation failed")
1169+
e.Validator = validationWrapper{
1170+
contextValidator: contextValidator{
1171+
validateCtx: func(stdContext.Context, any) error {
1172+
t.Fatal("Validate must not call an embedded ValidateCtx method")
1173+
return nil
1174+
},
1175+
},
1176+
err: wantErr,
1177+
}
1178+
assert.Same(t, wantErr, c.Validate(struct{}{}))
1179+
}
1180+
1181+
func TestContext_ValidateCtx_withContext(t *testing.T) {
1182+
ctx, cancel := stdContext.WithCancel(stdContext.Background())
1183+
defer cancel()
1184+
e := New()
1185+
c := e.NewContext(nil, nil)
1186+
c.SetRequest(httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx))
1187+
payload := &struct{ Name string }{Name: "Jon Snow"}
1188+
wantErr := errors.New("validation failed")
1189+
calls := 0
1190+
e.Validator = contextValidator{
1191+
validationFunc: func(any) error {
1192+
t.Fatal("Validate must not be called when ValidateCtx is implemented")
1193+
return nil
1194+
},
1195+
validateCtx: func(got stdContext.Context, i any) error {
1196+
calls++
1197+
assert.Same(t, ctx, got)
1198+
assert.Same(t, payload, i)
1199+
return wantErr
1200+
},
1201+
}
1202+
1203+
assert.Same(t, wantErr, c.ValidateCtx(payload))
1204+
assert.Equal(t, 1, calls)
1205+
1206+
cancel()
1207+
assert.Same(t, wantErr, c.ValidateCtx(payload))
1208+
assert.Equal(t, 2, calls)
1209+
}
1210+
1211+
func TestContext_ValidateCtx_requestReplacement(t *testing.T) {
1212+
firstCtx, cancelFirst := stdContext.WithCancel(stdContext.Background())
1213+
defer cancelFirst()
1214+
secondCtx, cancelSecond := stdContext.WithCancel(stdContext.Background())
1215+
defer cancelSecond()
1216+
1217+
e := New()
1218+
c := e.NewContext(httptest.NewRequest(http.MethodGet, "/", nil).WithContext(firstCtx), nil)
1219+
var gotCtx stdContext.Context
1220+
e.Validator = contextValidator{
1221+
validationFunc: func(any) error {
1222+
t.Fatal("Validate must not be called when ValidateCtx is implemented")
1223+
return nil
1224+
},
1225+
validateCtx: func(ctx stdContext.Context, _ any) error {
1226+
gotCtx = ctx
1227+
return nil
1228+
},
1229+
}
1230+
1231+
assert.NoError(t, c.ValidateCtx(struct{}{}))
1232+
assert.Same(t, firstCtx, gotCtx)
1233+
1234+
c.SetRequest(c.Request().WithContext(secondCtx))
1235+
assert.NoError(t, c.ValidateCtx(struct{}{}))
1236+
assert.Same(t, secondCtx, gotCtx)
1237+
}
1238+
1239+
func TestContext_ValidateCtx_withoutRequest(t *testing.T) {
1240+
e := New()
1241+
c := e.NewContext(nil, nil)
1242+
var gotCtx stdContext.Context
1243+
e.Validator = contextValidator{
1244+
validateCtx: func(ctx stdContext.Context, i any) error {
1245+
gotCtx = ctx
1246+
return nil
1247+
},
1248+
}
1249+
1250+
assert.NoError(t, c.ValidateCtx(struct{}{}))
1251+
if assert.NotNil(t, gotCtx) {
1252+
assert.NoError(t, gotCtx.Err())
1253+
assert.Nil(t, gotCtx.Done())
1254+
}
11261255
}
11271256

11281257
func TestContext_QueryString(t *testing.T) {

0 commit comments

Comments
 (0)