diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..9c6b032 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,13 @@ +kind: pipeline +type: docker +name: default + +steps: + - name: redis + image: redis:latest + - name: memcache + image: memcached:latest + - name: test + image: golang + commands: + - go test \ No newline at end of file diff --git a/cache.go b/cache.go index f976310..6bae6de 100644 --- a/cache.go +++ b/cache.go @@ -2,7 +2,9 @@ package cache import ( "errors" + "github.com/vmihailenco/msgpack/v4" "net/url" + "strconv" "strings" "time" ) @@ -10,6 +12,7 @@ import ( const ( Memcache = "memcache" Redis = "redis" + Memory = "memory" ) var ( @@ -18,7 +21,7 @@ var ( type CacheInterface interface { Has(key string) bool - Get(key string) ([]byte, error) + Get(key string, dst ...interface{}) ([]byte, error) Set(key string, val interface{}, ttl time.Duration) (err error) Del(key string) error } @@ -42,7 +45,64 @@ func New(uri string) (CacheInterface, error) { return NewMemcacheCache(MemcacheSettings{ Servers: strings.Split(u.Host, ","), }) + case Memory: + cleanupTime := query.Get("cleanupTime") + + if cleanupTime == "" { + cleanupTime = "30" + } + + i, err := strconv.Atoi(cleanupTime) + + if err != nil { + return nil, err + } + + return NewMemoryCache(time.Duration(i) * time.Second) } return nil, ErrInvalidDriver } + +func encodeValue(val interface{}) ([]byte, error) { + var v []byte + + if b, ok := val.([]byte); ok { + v = b + } else if s, ok := val.(string); ok { + b = []byte(s) + } else { + b, err := msgpack.Marshal(val) + + if err != nil { + return nil, err + } + + v = b + } + + return v, nil +} + +func decodeDst(b []byte, v interface{}) ([]byte, error) { + switch v := v.(type) { + case *[]byte: + if v != nil { + *v = b + return b, nil + } + case *string: + if v != nil { + *v = string(b) + return b, nil + } + } + + err := msgpack.Unmarshal(b, v) + + if err != nil { + return nil, err + } + + return b, nil +} diff --git a/go.mod b/go.mod index 79209e6..fb1350c 100644 --- a/go.mod +++ b/go.mod @@ -5,4 +5,6 @@ go 1.12 require ( github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b github.com/hoisie/redis v0.0.0-20160730154456-b5c6e81454e0 + github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/vmihailenco/msgpack/v4 v4.2.0 ) diff --git a/go.sum b/go.sum index 5207c33..ff4f405 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,37 @@ github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0= github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/hoisie/redis v0.0.0-20160730154456-b5c6e81454e0 h1:mjZV3MTu2A5gwfT5G9IIiLGdwZNciyVq5qqnmJJZ2JI= github.com/hoisie/redis v0.0.0-20160730154456-b5c6e81454e0/go.mod h1:pMYMxVaKJqCDC1JUg/XbPJ4/fSazB25zORpFzqsIGIc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/vmihailenco/msgpack/v4 v4.2.0 h1:c4L4gd938BvSjSsfr9YahJcvasEf5JZ9W7rcEXfgyys= +github.com/vmihailenco/msgpack/v4 v4.2.0/go.mod h1:Mu3B7ZwLd5nNOLVOKt9DecVl7IVg0xkDiEjk6CwMrww= +github.com/vmihailenco/tagparser v0.1.0 h1:u6yzKTY6gW/KxL/K2NTEQUOSXZipyGiIRarGjJKmQzU= +github.com/vmihailenco/tagparser v0.1.0/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/cache_memcache.go b/memcache.go similarity index 75% rename from cache_memcache.go rename to memcache.go index 5526d49..d3c1dc9 100644 --- a/cache_memcache.go +++ b/memcache.go @@ -1,7 +1,6 @@ package cache import ( - "encoding/json" "github.com/bradfitz/gomemcache/memcache" "time" ) @@ -28,31 +27,25 @@ func (mc *MemcacheCache) Has(key string) bool { return err != nil } -func (mc *MemcacheCache) Get(key string) ([]byte, error) { +func (mc *MemcacheCache) Get(key string, dst ...interface{}) ([]byte, error) { item, err := mc.backend.Get(key) if err != nil { return nil, err } + if len(dst) > 0 && dst[0] != nil { + return decodeDst(item.Value, dst[0]) + } + return item.Value, nil } func (mc *MemcacheCache) Set(key string, val interface{}, ttl time.Duration) error { - var v []byte + v, err := encodeValue(val) - if b, ok := val.([]byte); ok { - v = b - } else if s, ok := val.(string); ok { - b = []byte(s) - } else { - b, err := json.Marshal(val) - - if err != nil { - return err - } - - v = b + if err != nil { + return err } return mc.backend.Set(&memcache.Item{Key: key, Value: v, Expiration: int32(ttl.Seconds())}) diff --git a/memcache_test.go b/memcache_test.go new file mode 100644 index 0000000..e30af6d --- /dev/null +++ b/memcache_test.go @@ -0,0 +1,38 @@ +package cache + +import ( + "testing" + "time" +) + +func Test_MemcacheURI(t *testing.T) { + cache, err := New("memcache://memcache") + + if err != nil { + t.Fatal("Error creating cache:", err) + } + + if _, ok := cache.(*MemcacheCache); !ok { + t.Fatal("Cache is not instance of MemcacheCache") + } +} + +func Test_MemcacheStoreGet(t *testing.T) { + cache, err := New("memcache://memcache") + + if err != nil { + t.Fatal("Error creating cache:", err) + } + + obj := "test" + + cache.Set("test", obj, time.Minute) + + var new string + + cache.Get("test", &new) + + if obj != new { + t.Fatal("Expected", obj, "got", new) + } +} diff --git a/memory.go b/memory.go new file mode 100644 index 0000000..6f3001d --- /dev/null +++ b/memory.go @@ -0,0 +1,165 @@ +package cache + +import ( + "errors" + "github.com/patrickmn/go-cache" + "reflect" + "time" +) + +var ( + MemoryCacheNotExists = errors.New("item does not exist") +) + +type MemoryCache struct { + c *cache.Cache +} + +func NewMemoryCache(cleanupTime time.Duration) (CacheInterface, error) { + c := cache.New(1*time.Minute, cleanupTime) + + return &MemoryCache{c: c}, nil +} + +func (mc *MemoryCache) Has(key string) bool { + _, exists := mc.c.Get(key) + + return exists +} + +func (mc *MemoryCache) Get(key string, dst ...interface{}) ([]byte, error) { + item, exists := mc.c.Get(key) + + if !exists { + return nil, MemoryCacheNotExists + } + + v := dst[0] + + switch v := v.(type) { + case *string: + if v != nil { + *v = item.(string) + return nil, nil + } + case *[]byte: + if v != nil { + *v = item.([]byte) + return nil, nil + } + case *int: + if v != nil { + *v = item.(int) + return nil, nil + } + case *int8: + if v != nil { + *v = item.(int8) + return nil, nil + } + case *int16: + if v != nil { + *v = item.(int16) + return nil, nil + } + case *int32: + if v != nil { + *v = item.(int32) + return nil, nil + } + case *int64: + if v != nil { + *v = item.(int64) + return nil, nil + } + case *uint: + if v != nil { + *v = item.(uint) + return nil, nil + } + case *uint8: + if v != nil { + *v = item.(uint8) + return nil, nil + } + case *uint16: + if v != nil { + *v = item.(uint16) + return nil, nil + } + case *uint32: + if v != nil { + *v = item.(uint32) + return nil, nil + } + case *uint64: + if v != nil { + *v = item.(uint64) + return nil, nil + } + case *bool: + if v != nil { + *v = item.(bool) + return nil, nil + } + case *float32: + if v != nil { + *v = item.(float32) + return nil, nil + } + case *float64: + if v != nil { + *v = item.(float64) + return nil, nil + } + case *[]string: + *v = item.([]string) + return nil, nil + case *map[string]string: + *v = item.(map[string]string) + return nil, nil + case *map[string]interface{}: + *v = item.(map[string]interface{}) + return nil, nil + case *time.Duration: + if v != nil { + *v = item.(time.Duration) + return nil, nil + } + case *time.Time: + if v != nil { + *v = item.(time.Time) + return nil, nil + } + } + + vv := reflect.ValueOf(dst[0]) + + if !vv.IsValid() { + return nil, errors.New("dst pointer is not valid") + } + + if vv.Kind() != reflect.Ptr { + return nil, errors.New("dst pointer is not a pointer") + } + + vv = vv.Elem() + + if !vv.IsValid() { + return nil, errors.New("dst pointer is not a valid element") + } + + vv.Set(reflect.ValueOf(item)) + + return nil, nil +} + +func (mc *MemoryCache) Set(key string, val interface{}, ttl time.Duration) error { + mc.c.Set(key, val, ttl) + return nil +} + +func (mc *MemoryCache) Del(key string) error { + mc.c.Delete(key) + return nil +} diff --git a/memory_test.go b/memory_test.go new file mode 100644 index 0000000..dfe02f2 --- /dev/null +++ b/memory_test.go @@ -0,0 +1,38 @@ +package cache + +import ( + "testing" + "time" +) + +func Test_MemoryURI(t *testing.T) { + cache, err := New("memory://") + + if err != nil { + t.Fatal("Error creating cache:", err) + } + + if _, ok := cache.(*MemoryCache); !ok { + t.Fatal("Cache is not instance of MemoryCache") + } +} + +func Test_MemoryStoreGet(t *testing.T) { + cache, err := New("memory://") + + if err != nil { + t.Fatal("Error creating cache:", err) + } + + obj := "test" + + cache.Set("test", obj, time.Minute) + + var new string + + cache.Get("test", &new) + + if obj != new { + t.Fatal("Expected", obj, "got", new) + } +} diff --git a/cache_redis.go b/redis.go similarity index 70% rename from cache_redis.go rename to redis.go index b92482e..4384eee 100644 --- a/cache_redis.go +++ b/redis.go @@ -1,7 +1,6 @@ package cache import ( - "encoding/json" "github.com/hoisie/redis" "time" ) @@ -31,25 +30,25 @@ func (rc *RedisCache) Has(key string) bool { return b } -func (rc *RedisCache) Get(key string) ([]byte, error) { - return rc.c.Get(key) +func (rc *RedisCache) Get(key string, dst ...interface{}) ([]byte, error) { + b, err := rc.c.Get(key) + + if err != nil { + return nil, err + } + + if len(dst) > 0 && dst[0] != nil { + return decodeDst(b, dst[0]) + } + + return b, err } func (rc *RedisCache) Set(key string, val interface{}, ttl time.Duration) error { - var v []byte + v, err := encodeValue(val) - if b, ok := val.([]byte); ok { - v = b - } else if s, ok := val.(string); ok { - b = []byte(s) - } else { - b, err := json.Marshal(val) - - if err != nil { - return err - } - - v = b + if err != nil { + return err } return rc.c.Setex(key, int64(ttl.Seconds()), v) diff --git a/redis_test.go b/redis_test.go new file mode 100644 index 0000000..bbab887 --- /dev/null +++ b/redis_test.go @@ -0,0 +1,38 @@ +package cache + +import ( + "testing" + "time" +) + +func Test_RedisURI(t *testing.T) { + cache, err := New("redis://redis") + + if err != nil { + t.Fatal("Error creating cache:", err) + } + + if _, ok := cache.(*RedisCache); !ok { + t.Fatal("Cache is not instance of RedisCache") + } +} + +func Test_RedisStoreGet(t *testing.T) { + cache, err := New("redis://redis") + + if err != nil { + t.Fatal("Error creating cache:", err) + } + + obj := "test" + + cache.Set("test", obj, time.Minute) + + var new string + + cache.Get("test", &new) + + if obj != new { + t.Fatal("Expected", obj, "got", new) + } +}