Skip to content

Commit

Permalink
Response caching (#17)
Browse files Browse the repository at this point in the history
* added custom handler type

* Added tests

* Renaming

* Added base middelaware structure

* Added math matching

* Added base tests for cache middleware

* CacheableWriter did not save Content-Lenght

* Added tests for cache middlerware

* Removed config

* Ignored default configuration for repo

* Updated AssertIsDefined function

* Inject cache middleware factory from main function

* Provided cache configuration

* Added MiddlewareHandler interface

* Small refactoring

* Updated response writing

* Added default configs

* Added methods to cache midelware

* Cached responses only with 2xx status code

* Code refactoring
  • Loading branch information
Evgeny Abramovich authored and evg4b committed Nov 25, 2023
1 parent c3c6366 commit c8a5b62
Show file tree
Hide file tree
Showing 71 changed files with 1,788 additions and 903 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ dist/
uncors
.idea
node_modules
.uncors.yaml
12 changes: 7 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.19

require (
github.com/PuerkitoBio/purell v1.2.0
github.com/bmatcuk/doublestar/v4 v4.6.0
github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a
github.com/go-playground/assert/v2 v2.2.0
github.com/go-playground/validator/v10 v10.14.1
Expand All @@ -18,7 +19,7 @@ require (
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.16.0
github.com/stretchr/testify v1.8.4
golang.org/x/net v0.11.0
golang.org/x/net v0.12.0
)

require (
Expand All @@ -37,18 +38,19 @@ require (
github.com/lithammer/fuzzysearch v1.1.8 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/crypto v0.10.0 // indirect
golang.org/x/crypto v0.11.0 // indirect
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
golang.org/x/sys v0.9.0 // indirect
golang.org/x/term v0.9.0 // indirect
golang.org/x/text v0.10.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/term v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
175 changes: 14 additions & 161 deletions go.sum

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions internal/config/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package config

import (
"time"
)

type CacheGlobs []string

func (globs CacheGlobs) Clone() CacheGlobs {
if globs == nil {
return nil
}

cacheGlobs := make(CacheGlobs, 0, len(globs))
cacheGlobs = append(cacheGlobs, globs...)

return cacheGlobs
}

type CacheConfig struct {
ExpirationTime time.Duration `mapstructure:"expiration-time"`
ClearTime time.Duration `mapstructure:"clear-time"`
Methods []string `mapstructure:"methods"`
}

func (config *CacheConfig) Clone() *CacheConfig {
var methods []string
if config.Methods != nil {
methods = append(methods, config.Methods...)
}

return &CacheConfig{
ExpirationTime: config.ExpirationTime,
ClearTime: config.ClearTime,
Methods: methods,
}
}
51 changes: 51 additions & 0 deletions internal/config/cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package config_test

import (
"net/http"
"testing"
"time"

"github.com/evg4b/uncors/internal/config"
"github.com/stretchr/testify/assert"
)

func TestCacheGlobsClone(t *testing.T) {
globs := config.CacheGlobs{
"/api/**",
"/constants",
"/translations",
"/**/*.js",
}

cacheGlobs := globs.Clone()

t.Run("not same", func(t *testing.T) {
assert.NotSame(t, globs, cacheGlobs)
})

t.Run("equals values", func(t *testing.T) {
assert.EqualValues(t, globs, cacheGlobs)
})
}

func TestCacheConfigClone(t *testing.T) {
cacheConfig := &config.CacheConfig{
ExpirationTime: 5 * time.Minute,
ClearTime: 30 * time.Second,
Methods: []string{http.MethodGet, http.MethodPost},
}

clonedCacheConfig := cacheConfig.Clone()

t.Run("not same", func(t *testing.T) {
assert.NotSame(t, cacheConfig, clonedCacheConfig)
})

t.Run("equals values", func(t *testing.T) {
assert.EqualValues(t, cacheConfig, clonedCacheConfig)
})

t.Run("not same methods", func(t *testing.T) {
assert.NotSame(t, cacheConfig.Methods, clonedCacheConfig.Methods)
})
}
19 changes: 10 additions & 9 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package config
import (
"fmt"

"github.com/evg4b/uncors/internal/config/hooks"
"github.com/mitchellh/mapstructure"
"github.com/spf13/pflag"
"github.com/spf13/viper"
Expand All @@ -15,13 +14,14 @@ const (
)

type UncorsConfig struct {
HTTPPort int `mapstructure:"http-port" validate:"required"`
Mappings Mappings `mapstructure:"mappings" validate:"required"`
Proxy string `mapstructure:"proxy"`
Debug bool `mapstructure:"debug"`
HTTPSPort int `mapstructure:"https-port"`
CertFile string `mapstructure:"cert-file"`
KeyFile string `mapstructure:"key-file"`
HTTPPort int `mapstructure:"http-port" validate:"required"`
Mappings Mappings `mapstructure:"mappings" validate:"required"`
Proxy string `mapstructure:"proxy"`
Debug bool `mapstructure:"debug"`
HTTPSPort int `mapstructure:"https-port"`
CertFile string `mapstructure:"cert-file"`
KeyFile string `mapstructure:"key-file"`
CacheConfig CacheConfig `mapstructure:"cache-config"`
}

func (config *UncorsConfig) IsHTTPSEnabled() bool {
Expand Down Expand Up @@ -51,10 +51,11 @@ func LoadConfiguration(viperInstance *viper.Viper, args []string) (*UncorsConfig

configOption := viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
mapstructure.StringToSliceHookFunc(","),
hooks.StringToTimeDurationHookFunc(),
StringToTimeDurationHookFunc(),
URLMappingHookFunc(),
))

setDefaultValues(viperInstance)
if err := viperInstance.Unmarshal(configuration, configOption); err != nil {
return nil, fmt.Errorf("filed parsing config: %w", err)
}
Expand Down
34 changes: 34 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package config_test

import (
"net/http"
"testing"
"time"

"github.com/evg4b/uncors/internal/config"
"github.com/evg4b/uncors/testing/testconstants"
Expand Down Expand Up @@ -47,6 +49,12 @@ debug: true
https-port: 8081
cert-file: /etc/certificates/cert-file.pem
key-file: /etc/certificates/key-file.key
cache-config:
expiration-time: 1h
clear-time: 30m
methods:
- GET
- POST
`
)

Expand Down Expand Up @@ -89,6 +97,11 @@ func TestLoadConfiguration(t *testing.T) {
HTTPPort: 80,
HTTPSPort: 443,
Mappings: config.Mappings{},
CacheConfig: config.CacheConfig{
ExpirationTime: config.DefaultExpirationTime,
ClearTime: config.DefaultClearTime,
Methods: []string{http.MethodGet},
},
},
},
{
Expand All @@ -100,6 +113,11 @@ func TestLoadConfiguration(t *testing.T) {
Mappings: config.Mappings{
{From: testconstants.HTTPLocalhost, To: testconstants.HTTPSGithub},
},
CacheConfig: config.CacheConfig{
ExpirationTime: config.DefaultExpirationTime,
ClearTime: config.DefaultClearTime,
Methods: []string{http.MethodGet},
},
},
},
{
Expand Down Expand Up @@ -139,6 +157,14 @@ func TestLoadConfiguration(t *testing.T) {
HTTPSPort: 8081,
CertFile: testconstants.CertFilePath,
KeyFile: testconstants.KeyFilePath,
CacheConfig: config.CacheConfig{
ExpirationTime: time.Hour,
ClearTime: 30 * time.Minute,
Methods: []string{
http.MethodGet,
http.MethodPost,
},
},
},
},
{
Expand Down Expand Up @@ -186,6 +212,14 @@ func TestLoadConfiguration(t *testing.T) {
HTTPSPort: 8081,
CertFile: testconstants.CertFilePath,
KeyFile: testconstants.KeyFilePath,
CacheConfig: config.CacheConfig{
ExpirationTime: time.Hour,
ClearTime: 30 * time.Minute,
Methods: []string{
http.MethodGet,
http.MethodPost,
},
},
},
},
}
Expand Down
19 changes: 19 additions & 0 deletions internal/config/default.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package config

import (
"net/http"
"time"

"github.com/spf13/viper"
)

const (
DefaultExpirationTime = 30 * time.Minute
DefaultClearTime = 30 * time.Minute
)

func setDefaultValues(instance *viper.Viper) {
instance.SetDefault("cache-config.expiration-time", DefaultExpirationTime)
instance.SetDefault("cache-config.clear-time", DefaultClearTime)
instance.SetDefault("cache-config.methods", []string{http.MethodGet})
}
3 changes: 1 addition & 2 deletions internal/config/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"strconv"
"strings"

"github.com/evg4b/uncors/internal/config/hooks"
"github.com/evg4b/uncors/pkg/urlx"
"github.com/mitchellh/mapstructure"
"github.com/samber/lo"
Expand Down Expand Up @@ -51,7 +50,7 @@ func readURLMapping(config *viper.Viper, configuration *UncorsConfig) error {

func decodeConfig[T any](data any, mapping *T, decodeFuncs ...mapstructure.DecodeHookFunc) error {
hook := mapstructure.ComposeDecodeHookFunc(
hooks.StringToTimeDurationHookFunc(),
StringToTimeDurationHookFunc(),
mapstructure.StringToSliceHookFunc(","),
mapstructure.ComposeDecodeHookFunc(decodeFuncs...),
)
Expand Down
2 changes: 2 additions & 0 deletions internal/config/mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type Mapping struct {
To string `mapstructure:"to"`
Statics StaticDirectories `mapstructure:"statics"`
Mocks Mocks `mapstructure:"mocks"`
Cache CacheGlobs `mapstructure:"cache"`
}

func (u *Mapping) Clone() Mapping {
Expand All @@ -20,6 +21,7 @@ func (u *Mapping) Clone() Mapping {
To: u.To,
Statics: u.Statics.Clone(),
Mocks: u.Mocks.Clone(),
Cache: u.Cache.Clone(),
}
}

Expand Down
3 changes: 3 additions & 0 deletions internal/config/mappings.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ func (mappings Mappings) String() string {
for _, static := range mapping.Statics {
builder.WriteString(sfmt.Sprintf(" static: %s => %s\n", static.Path, static.Dir))
}
for _, cacheGlob := range mapping.Cache {
builder.WriteString(sfmt.Sprintf(" cache: %s\n", cacheGlob))
}
}

builder.WriteString("\n")
Expand Down
20 changes: 0 additions & 20 deletions internal/config/model.go → internal/config/mock.go
Original file line number Diff line number Diff line change
@@ -1,30 +1,10 @@
package config

import (
"time"

"github.com/evg4b/uncors/internal/helpers"
"github.com/samber/lo"
)

type Response struct {
Code int `mapstructure:"code"`
Headers map[string]string `mapstructure:"headers"`
Raw string `mapstructure:"raw"`
File string `mapstructure:"file"`
Delay time.Duration `mapstructure:"delay"`
}

func (r *Response) Clone() Response {
return Response{
Code: r.Code,
Headers: helpers.CloneMap(r.Headers),
Raw: r.Raw,
File: r.File,
Delay: r.Delay,
}
}

type Mock struct {
Path string `mapstructure:"path"`
Method string `mapstructure:"method"`
Expand Down
28 changes: 0 additions & 28 deletions internal/config/model_test.go → internal/config/mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,12 @@ package config_test
import (
"net/http"
"testing"
"time"

"github.com/evg4b/uncors/internal/config"
"github.com/go-http-utils/headers"
"github.com/stretchr/testify/assert"
)

func TestResponseClone(t *testing.T) {
response := config.Response{
Code: http.StatusOK,
Headers: map[string]string{
headers.ContentType: "plain/text",
headers.CacheControl: "none",
},
Raw: "this is plain text",
File: "~/projects/uncors/response/demo.json",
Delay: time.Hour,
}

clonedResponse := response.Clone()

t.Run("not same", func(t *testing.T) {
assert.NotSame(t, &response, &clonedResponse)
})

t.Run("equals values", func(t *testing.T) {
assert.EqualValues(t, response, clonedResponse)
})

t.Run("not same Headers map", func(t *testing.T) {
assert.NotSame(t, &response.Headers, &clonedResponse.Headers)
})
}

func TestMockClone(t *testing.T) {
mock := config.Mock{
Path: "/constants",
Expand Down
Loading

0 comments on commit c8a5b62

Please sign in to comment.