Skip to content

Commit

Permalink
feat: user-configurable querycache and formcache
Browse files Browse the repository at this point in the history
  • Loading branch information
araujo88 committed Aug 11, 2023
1 parent d16fdb1 commit 6d7e535
Show file tree
Hide file tree
Showing 3 changed files with 245 additions and 14 deletions.
67 changes: 53 additions & 14 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -456,15 +456,28 @@ func (c *Context) QueryArray(key string) (values []string) {
}

func (c *Context) initQueryCache() {
if c.queryCache == nil {
if c.Request != nil {
c.queryCache = c.Request.URL.Query()
} else {
c.queryCache = url.Values{}
}
if !c.engine.cacheConfig.EnableQueryCache || c.queryCache != nil {
return
}

if c.Request != nil {
c.queryCache = c.Request.URL.Query()
} else {
c.queryCache = url.Values{}
}
}

// SetQuery sets the values for a given query key in the context's query cache.
// It ensures that the query cache is initialized before setting the values.
// If the query key already exists, its values will be overwritten with the provided values.
func (c *Context) SetQuery(key string, values []string) {
if !c.engine.cacheConfig.EnableQueryCache {
return // If query caching is disabled, just return without modifying the cache
}
c.initQueryCache()
c.queryCache[key] = values
}

// GetQueryArray returns a slice of strings for a given query key, plus
// a boolean value whether at least one value exists for the given key.
func (c *Context) GetQueryArray(key string) (values []string, ok bool) {
Expand Down Expand Up @@ -526,16 +539,42 @@ func (c *Context) PostFormArray(key string) (values []string) {
}

func (c *Context) initFormCache() {
if c.formCache == nil {
c.formCache = make(url.Values)
req := c.Request
if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
if !errors.Is(err, http.ErrNotMultipart) {
debugPrint("error on parse multipart form array: %v", err)
}
if !c.engine.cacheConfig.EnableFormCache || c.formCache != nil {
return
}

if c.Request == nil {
return // If the Request is nil, exit early to avoid a nil pointer dereference
}

c.formCache = make(url.Values)
if err := c.Request.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
if !errors.Is(err, http.ErrNotMultipart) {
debugPrint("error on parse multipart form array: %v", err)
}
c.formCache = req.PostForm
}
c.formCache = c.Request.PostForm
}

// SetForm sets the values for a given form key in the context's form cache.
// It ensures that the form cache is initialized before setting the values.
// If the form key already exists, its values will be overwritten with the provided values.
func (c *Context) SetForm(key string, values []string) {
c.initFormCache()
if c.formCache != nil { // Only set values if the form cache is enabled
c.formCache[key] = values
}
}

// GetForm retrieves the values associated with a given form key from the context's form cache.
// If the form cache has not been initialized, it does so first.
// If the form key does not exist, an empty slice is returned.
func (c *Context) GetForm(key string) []string {
c.initFormCache()
if c.formCache == nil {
return []string{} // Return an empty slice when the form cache is disabled
}
return c.formCache[key]
}

// GetPostFormArray returns a slice of strings for a given form key, plus
Expand Down
170 changes: 170 additions & 0 deletions context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,65 @@ func TestContextQueryAndPostForm(t *testing.T) {
assert.Equal(t, 0, len(dicts))
}

func TestSetQuery(t *testing.T) {
// Create a Context instance with initialized query cache
c := &Context{
engine: &Engine{
cacheConfig: NewCacheConfig(true, true),
},
Request: &http.Request{
URL: &url.URL{
RawQuery: "existingKey=value1",
},
},
}

// Key and values to set
key := "testKey"
values := []string{"value1", "value2"}

// Call SetQuery method
c.SetQuery(key, values)

// Retrieve values from the query cache
retrievedValues, ok := c.queryCache[key]

// Check if the values were correctly set
if !ok {
t.Errorf("Key %s not found in query cache", key)
}

if !reflect.DeepEqual(retrievedValues, values) {
t.Errorf("Expected values %v, got %v", values, retrievedValues)
}
}

func TestSetQueryWithCacheDisabled(t *testing.T) {
// Create a Context instance with query cache disabled
c := &Context{
engine: &Engine{
cacheConfig: NewCacheConfig(false, false), // Disabling both query and form caches
},
Request: &http.Request{
URL: &url.URL{
RawQuery: "existingKey=value1",
},
},
}

// Key and values to set
key := "testKey"
values := []string{"value1", "value2"}

// Call SetQuery method
c.SetQuery(key, values)

// Since the query cache is disabled, we expect the query cache to be nil
if c.queryCache != nil {
t.Errorf("Expected query cache to be nil, got %v", c.queryCache)
}
}

func TestContextPostFormMultipart(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request = createMultipartRequest()
Expand Down Expand Up @@ -613,6 +672,117 @@ func TestContextPostFormMultipart(t *testing.T) {
assert.Equal(t, 0, len(dicts))
}

func TestSetForm(t *testing.T) {
req, _ := http.NewRequest("POST", "/", nil)
c := &Context{
Request: req,
engine: &Engine{
cacheConfig: NewCacheConfig(true, true), // Enabling both query and form caches
},
}

key := "testKey"
values := []string{"value1", "value2"}

c.SetForm(key, values)
retrievedValues := c.formCache[key]

if !reflect.DeepEqual(values, retrievedValues) {
t.Errorf("Expected values %v, got %v", values, retrievedValues)
}
}

func TestGetForm(t *testing.T) {
// Create a Context instance with initialized form cache
c := &Context{
engine: &Engine{
cacheConfig: NewCacheConfig(true, true), // Assuming this initializes both caches
},
formCache: map[string][]string{
"existingKey": {"existingValue"},
},
}

// Test retrieval of existing key
existingKey := "existingKey"
retrievedValues := c.GetForm(existingKey)
if !reflect.DeepEqual(retrievedValues, []string{"existingValue"}) {
t.Errorf("Expected values %v, got %v", []string{"existingValue"}, retrievedValues)
}

// Test retrieval of non-existing key
nonExistingKey := "nonExistingKey"
retrievedValues = c.GetForm(nonExistingKey)
if len(retrievedValues) != 0 {
t.Errorf("Expected an empty slice, got %v", retrievedValues)
}
}

func TestSetFormWithCacheDisabled(t *testing.T) {
c := &Context{
engine: &Engine{
cacheConfig: NewCacheConfig(false, false), // Disabling both query and form caches
},
}

key := "testKey"
values := []string{"value1", "value2"}

c.SetForm(key, values)

// Since the form cache is disabled, we expect the form cache to be nil
if c.formCache != nil {
t.Errorf("Expected form cache to be nil, got %v", c.formCache)
}
}

func TestGetFormWithCacheDisabled(t *testing.T) {
c := &Context{
engine: &Engine{
cacheConfig: NewCacheConfig(false, false), // Disabling both query and form caches
},
}

key := "existingKey"

// Call GetForm method
retrievedValues := c.GetForm(key)

// Since the form cache is disabled and no pre-existing values were set, we expect the retrieved values to be an empty slice
if len(retrievedValues) != 0 {
t.Errorf("Expected an empty slice, got %v", retrievedValues)
}
}

func TestChangeUrlParam(t *testing.T) {
ChangeUrlParam := func(c *Context) {
params, _ := url.ParseQuery(c.Request.URL.RawQuery)
params.Set("xxx", "yyy")
c.Request.URL.RawQuery = params.Encode()
}

r := Default()
r.Use(ChangeUrlParam)
r.GET("/test", func(c *Context) {
// Dummy handler
})

// Create a test request with original parameters
req, err := http.NewRequest("GET", "/test?param1=value1", nil)
if err != nil {
t.Fatal(err)
}

// Record the response
w := httptest.NewRecorder()
r.ServeHTTP(w, req)

// Check that the URL parameter "xxx" has been set to "yyy"
if got := req.URL.Query().Get("xxx"); got != "yyy" {
t.Errorf("ChangeUrlParam() = %v, want %v", got, "yyy")
}
}

func TestContextSetCookie(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.SetSameSite(http.SameSiteLaxMode)
Expand Down
22 changes: 22 additions & 0 deletions gin.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,22 @@ const (
PlatformCloudflare = "CF-Connecting-IP"
)

// CacheConfig represents the configuration options for managing query and form caches
// within a Gin context. It provides flags to enable or disable the caching mechanisms
// for query parameters and form data, allowing for greater control over caching behavior.
type CacheConfig struct {
EnableQueryCache bool
EnableFormCache bool
}

// NewCacheConfig returns a new instance of CacheConfig.
func NewCacheConfig(enableQueryCache, enableFormCache bool) CacheConfig {
return CacheConfig{
EnableQueryCache: enableQueryCache,
EnableFormCache: enableFormCache,
}
}

// Engine is the framework's instance, it contains the muxer, middleware and configuration settings.
// Create an instance of Engine, by using New() or Default()
type Engine struct {
Expand Down Expand Up @@ -168,6 +184,7 @@ type Engine struct {
maxSections uint16
trustedProxies []string
trustedCIDRs []*net.IPNet
cacheConfig CacheConfig
}

var _ IRouter = (*Engine)(nil)
Expand Down Expand Up @@ -204,6 +221,7 @@ func New() *Engine {
secureJSONPrefix: "while(1);",
trustedProxies: []string{"0.0.0.0/0", "::/0"},
trustedCIDRs: defaultTrustedCIDRs,
cacheConfig: CacheConfig{true, true},
}
engine.RouterGroup.engine = engine
engine.pool.New = func() any {
Expand Down Expand Up @@ -649,6 +667,10 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
serveError(c, http.StatusNotFound, default404Body)
}

func (engine *Engine) SetCacheConfig(config CacheConfig) {
engine.cacheConfig = config
}

var mimePlain = []string{MIMEPlain}

func serveError(c *Context, code int, defaultMessage []byte) {
Expand Down

0 comments on commit 6d7e535

Please sign in to comment.