diff --git a/context.go b/context.go index a8ee4b51d..ed6387a26 100644 --- a/context.go +++ b/context.go @@ -455,11 +455,17 @@ func (c *Context) Cookies() []*http.Cookie { // Get retrieves data from the context. // Method returns any(nil) when key does not exist which is different from typed nil (eg. []byte(nil)). func (c *Context) Get(key string) any { + v, _ := c.get(key) + return v +} + +// get retrieves a context value and reports whether its key exists. +func (c *Context) get(key string) (any, bool) { // Unlock without defer to avoid the deferred-call overhead on this hot path. c.lock.RLock() - v := c.store[key] + v, ok := c.store[key] c.lock.RUnlock() - return v + return v, ok } // Set saves data in the context. diff --git a/context_generic.go b/context_generic.go index 7cf8b296c..1c74e2d1d 100644 --- a/context_generic.go +++ b/context_generic.go @@ -11,13 +11,14 @@ var ErrNonExistentKey = errors.New("non existent key") // ErrInvalidKeyType is error that is returned when the value is not castable to expected type. var ErrInvalidKeyType = errors.New("invalid key type") -// ContextGet retrieves a value from the context store or ErrNonExistentKey error the key is missing. +// ContextGet retrieves a value from the context store or ErrNonExistentKey error if the key is missing. // Returns ErrInvalidKeyType error if the value is not castable to type T. func ContextGet[T any](c *Context, key string) (T, error) { - c.lock.RLock() - defer c.lock.RUnlock() + return contextValue[T](c, key) +} - val, ok := c.store[key] +func contextValue[T any](c *Context, key string) (T, error) { + val, ok := c.get(key) if !ok { var zero T return zero, ErrNonExistentKey @@ -35,7 +36,11 @@ func ContextGet[T any](c *Context, key string) (T, error) { // ContextGetOr retrieves a value from the context store or returns a default value when the key // is missing. Returns ErrInvalidKeyType error if the value is not castable to type T. func ContextGetOr[T any](c *Context, key string, defaultValue T) (T, error) { - typed, err := ContextGet[T](c, key) + return contextValueOr(c, key, defaultValue) +} + +func contextValueOr[T any](c *Context, key string, defaultValue T) (T, error) { + typed, err := contextValue[T](c, key) if err == ErrNonExistentKey { return defaultValue, nil } diff --git a/context_generic_go127.go b/context_generic_go127.go new file mode 100644 index 000000000..75a27faf3 --- /dev/null +++ b/context_generic_go127.go @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors + +//go:build go1.27 + +package echo + +// Value retrieves a value from the context store or ErrNonExistentKey error if the key is missing. +// Returns ErrInvalidKeyType error if the value is not castable to type T. +func (c *Context) Value[T any](key string) (T, error) { + return contextValue[T](c, key) +} + +// ValueOr retrieves a value from the context store or returns a default value when the key +// is missing. Returns ErrInvalidKeyType error if the value is not castable to type T. +func (c *Context) ValueOr[T any](key string, defaultValue T) (T, error) { + return contextValueOr(c, key, defaultValue) +} diff --git a/context_generic_go127_test.go b/context_generic_go127_test.go new file mode 100644 index 000000000..6f650f143 --- /dev/null +++ b/context_generic_go127_test.go @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors + +//go:build go1.27 + +package echo + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestContextValueOK(t *testing.T) { + c := NewContext(nil, nil) + + c.Set("key", int64(123)) + + v, err := c.Value[int64]("key") + assert.NoError(t, err) + assert.Equal(t, int64(123), v) +} + +func TestContextValueNonExistentKey(t *testing.T) { + c := NewContext(nil, nil) + + v, err := c.Value[int64]("nope") + assert.ErrorIs(t, err, ErrNonExistentKey) + assert.Equal(t, int64(0), v) +} + +func TestContextValueInvalidCast(t *testing.T) { + c := NewContext(nil, nil) + + c.Set("key", int64(123)) + + v, err := c.Value[bool]("key") + assert.ErrorIs(t, err, ErrInvalidKeyType) + assert.False(t, v) +} + +func TestContextValueStoredNilHasInvalidType(t *testing.T) { + c := NewContext(nil, nil) + + c.Set("key", nil) + + v, err := c.Value[any]("key") + assert.ErrorIs(t, err, ErrInvalidKeyType) + assert.Nil(t, v) +} + +func TestContextValueOrOK(t *testing.T) { + c := NewContext(nil, nil) + + c.Set("key", int64(123)) + + v, err := c.ValueOr[int64]("key", 999) + assert.NoError(t, err) + assert.Equal(t, int64(123), v) +} + +func TestContextValueOrNonExistentKey(t *testing.T) { + c := NewContext(nil, nil) + + v, err := c.ValueOr[int64]("nope", 999) + assert.NoError(t, err) + assert.Equal(t, int64(999), v) +} + +func TestContextValueOrInvalidCast(t *testing.T) { + c := NewContext(nil, nil) + + c.Set("key", int64(123)) + + v, err := c.ValueOr[float32]("key", float32(999)) + assert.ErrorIs(t, err, ErrInvalidKeyType) + assert.Equal(t, float32(0), v) +}