From 41d0ef3d97fcf3f0749275723d79a0b4e1de6873 Mon Sep 17 00:00:00 2001 From: Tyler Date: Sat, 4 Feb 2023 20:22:41 -0500 Subject: [PATCH] Initial v2 version, better testing, updates --- .drone.yml | 4 +- README.md | 4 +- cache.go | 138 ++++++++------------ cache_test.go | 39 ++++++ cacheinterface_suite_test.go | 13 ++ driver/lru/lru.go | 67 ++++++++++ driver/lru/lru_suite_test.go | 13 ++ driver/lru/lru_test.go | 66 ++++++++++ driver/memcache/memcache.go | 68 ++++++++++ driver/memory/memory.go | 199 +++++++++++++++++++++++++++++ driver/memory/memory_suite_test.go | 13 ++ driver/memory/memory_test.go | 65 ++++++++++ driver/redis/redis.go | 73 +++++++++++ encoder/binary.go | 32 +++++ encoder/encoding.go | 66 ++++++++++ go.mod | 25 +++- go.sum | 36 +++++- lru.go | 46 ------- lru_test.go | 96 -------------- memcache.go | 58 --------- memcache_test.go | 35 ----- memory.go | 181 -------------------------- memory_test.go | 96 -------------- options.go | 169 ++++++++++++++++++++++++ options_test.go | 95 ++++++++++++++ redis.go | 61 --------- redis_test.go | 17 --- 27 files changed, 1094 insertions(+), 681 deletions(-) create mode 100644 cache_test.go create mode 100644 cacheinterface_suite_test.go create mode 100644 driver/lru/lru.go create mode 100644 driver/lru/lru_suite_test.go create mode 100644 driver/lru/lru_test.go create mode 100644 driver/memcache/memcache.go create mode 100644 driver/memory/memory.go create mode 100644 driver/memory/memory_suite_test.go create mode 100644 driver/memory/memory_test.go create mode 100644 driver/redis/redis.go create mode 100644 encoder/binary.go create mode 100644 encoder/encoding.go delete mode 100644 lru.go delete mode 100644 lru_test.go delete mode 100644 memcache.go delete mode 100644 memcache_test.go delete mode 100644 memory.go delete mode 100644 memory_test.go create mode 100644 options.go create mode 100644 options_test.go delete mode 100644 redis.go delete mode 100644 redis_test.go diff --git a/.drone.yml b/.drone.yml index ab65c02..3899c8a 100644 --- a/.drone.yml +++ b/.drone.yml @@ -6,4 +6,6 @@ steps: - name: test image: golang commands: - - go test \ No newline at end of file + - go mod download + - go install github.com/onsi/ginkgo/v2/ginkgo + - ginkgo -r . \ No newline at end of file diff --git a/README.md b/README.md index 4a535ec..9082415 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,8 @@ The CacheInterface interface has all methods. All drivers will implement this on ```go type CacheInterface interface { Has(key string) bool - Get(key string, dst ...interface{}) ([]byte, error) - Set(key string, val interface{}, ttl time.Duration) (err error) + Get(key string, dst ...any) ([]byte, error) + Set(key string, val any, ttl time.Duration) (err error) Del(key string) error } ``` diff --git a/cache.go b/cache.go index 6daae6c..f8a864b 100644 --- a/cache.go +++ b/cache.go @@ -2,33 +2,51 @@ package cache import ( "errors" - "github.com/vmihailenco/msgpack/v4" + "meow.tf/go/cacheinterface/v2/driver/lru" + "meow.tf/go/cacheinterface/v2/driver/memcache" + "meow.tf/go/cacheinterface/v2/driver/memory" + "meow.tf/go/cacheinterface/v2/driver/redis" "net" "net/url" - "strconv" "strings" "time" ) +type Type string + const ( - Memcache = "memcache" - Redis = "redis" - Memory = "memory" - Lru = "lru" + TypeMemcache Type = "memcache" + TypeRedis Type = "redis" + TypeMemory Type = "memory" + TypeLru Type = "lru" ) var ( ErrInvalidDriver = errors.New("invalid driver") ) -type CacheInterface interface { +func init() { + var err error + + if err != nil { + panic(err) + } +} + +type Driver interface { Has(key string) bool - Get(key string, dst ...interface{}) ([]byte, error) - Set(key string, val interface{}, ttl time.Duration) (err error) + Get(key string, dst any) error + GetBytes(key string) ([]byte, error) + Set(key string, val any, ttl time.Duration) (err error) Del(key string) error } -func New(uri string) (CacheInterface, error) { +type ( + Marshaller func(val any) ([]byte, error) + Unmarshaler func(b []byte, dest any) error +) + +func New(uri string) (Driver, error) { u, err := url.Parse(uri) if err != nil { @@ -37,94 +55,50 @@ func New(uri string) (CacheInterface, error) { query := u.Query() - switch u.Scheme { - case Redis: + switch Type(u.Scheme) { + case TypeRedis: port := u.Port() if port == "" { port = "6379" } - return NewRedisCache(RedisSettings{ - Address: net.JoinHostPort(u.Hostname(), port), - Password: query.Get("password"), - }) - case Memcache: - return NewMemcacheCache(MemcacheSettings{ - Servers: strings.Split(u.Host, ","), - }) - case Memory: - cleanupTime := query.Get("cleanupTime") + var settings redis.Options - if cleanupTime == "" { - cleanupTime = "30" - } - - i, err := strconv.Atoi(cleanupTime) - - if err != nil { + if err = decodeQuery(query, &settings); err != nil { return nil, err } - return NewMemoryCache(time.Duration(i) * time.Second) - case Lru: - size := query.Get("size") + settings.Address = net.JoinHostPort(u.Hostname(), port) - if size == "" { - size = "128" - } + return redis.New(settings) + case TypeMemcache: + var options memcache.Options - i, err := strconv.Atoi(size) - - if err != nil { + if err = decodeQuery(query, &options); err != nil { return nil, err } - return NewLruCache(i) + options.Servers = strings.Split(u.Host, ",") + + return memcache.New(options) + case TypeMemory: + var options memory.Options + + if err = decodeQuery(query, &options); err != nil { + return nil, err + } + + return memory.New(options) + case TypeLru: + var options lru.Options + + if err = decodeQuery(query, &options); err != nil { + return nil, err + } + + return lru.New(options) } 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/cache_test.go b/cache_test.go new file mode 100644 index 0000000..e34627b --- /dev/null +++ b/cache_test.go @@ -0,0 +1,39 @@ +package cache + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "meow.tf/go/cacheinterface/v2/driver/lru" + "meow.tf/go/cacheinterface/v2/driver/memcache" + "meow.tf/go/cacheinterface/v2/driver/memory" + "meow.tf/go/cacheinterface/v2/driver/redis" +) + +var _ = Describe("Cache test", func() { + Context("URI Parsing", func() { + It("Should parse a Redis URI", func() { + c, err := New("redis://") + + Expect(err).To(BeNil()) + Expect(c).To(BeAssignableToTypeOf(&redis.Cache{})) + }) + It("Should parse a Memcache URI", func() { + c, err := New("memcache://") + + Expect(err).To(BeNil()) + Expect(c).To(BeAssignableToTypeOf(&memcache.Cache{})) + }) + It("Should parse a LRU URI", func() { + c, err := New("lru://") + + Expect(err).To(BeNil()) + Expect(c).To(BeAssignableToTypeOf(&lru.Cache{})) + }) + It("Should parse a Memory URI", func() { + c, err := New("memory://") + + Expect(err).To(BeNil()) + Expect(c).To(BeAssignableToTypeOf(&memory.Cache{})) + }) + }) +}) diff --git a/cacheinterface_suite_test.go b/cacheinterface_suite_test.go new file mode 100644 index 0000000..40a6b7e --- /dev/null +++ b/cacheinterface_suite_test.go @@ -0,0 +1,13 @@ +package cache_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestCacheInterface(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "CacheInterface Suite") +} diff --git a/driver/lru/lru.go b/driver/lru/lru.go new file mode 100644 index 0000000..945f8ad --- /dev/null +++ b/driver/lru/lru.go @@ -0,0 +1,67 @@ +package lru + +import ( + "github.com/hashicorp/golang-lru" + "meow.tf/go/cacheinterface/v2/driver/memory" + "meow.tf/go/cacheinterface/v2/encoder" + "time" +) + +type Options struct { + Encoder encoder.Encoder `query:"encoder" default:"msgpack"` + Size int `query:"size" default:"128"` +} + +type Cache struct { + options Options + c *lru.Cache +} + +func New(options Options) (*Cache, error) { + c, err := lru.New(options.Size) + + if err != nil { + return nil, err + } + + return &Cache{ + options: options, + c: c, + }, nil +} + +func (mc *Cache) Has(key string) bool { + _, exists := mc.c.Get(key) + + return exists +} + +func (mc *Cache) Get(key string, dst any) error { + item, exists := mc.c.Get(key) + + if !exists { + return memory.ErrNotExist + } + + return memory.CacheGet(item, dst) +} + +func (mc *Cache) GetBytes(key string) ([]byte, error) { + item, exists := mc.c.Get(key) + + if !exists { + return nil, memory.ErrNotExist + } + + return memory.CacheGetBytes(mc.options.Encoder, item) +} + +func (mc *Cache) Set(key string, val any, ttl time.Duration) error { + mc.c.Add(key, val) + return nil +} + +func (mc *Cache) Del(key string) error { + mc.c.Remove(key) + return nil +} diff --git a/driver/lru/lru_suite_test.go b/driver/lru/lru_suite_test.go new file mode 100644 index 0000000..8461eb7 --- /dev/null +++ b/driver/lru/lru_suite_test.go @@ -0,0 +1,13 @@ +package lru_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestLru(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Lru Suite") +} diff --git a/driver/lru/lru_test.go b/driver/lru/lru_test.go new file mode 100644 index 0000000..3333909 --- /dev/null +++ b/driver/lru/lru_test.go @@ -0,0 +1,66 @@ +package lru + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "meow.tf/go/cacheinterface/v2/encoder" + "time" +) + +var _ = Describe("LRU driver", func() { + Context("Basic operations", func() { + var ( + cache *Cache + ) + BeforeEach(func() { + var err error + + cache, err = New(Options{ + Encoder: encoder.JSON, + Size: 128, + }) + + Expect(err).To(BeNil()) + }) + It("Should store a value in the cache", func() { + value := "test" + Expect(cache.Set("test", value, time.Minute)).To(BeNil()) + }) + It("Should get a value from the cache", func() { + value := "test" + var newValue string + + Expect(cache.Set("test", value, time.Minute)).To(BeNil()) + Expect(cache.Get("test", &newValue)).To(BeNil()) + Expect(newValue).To(Equal(value)) + }) + It("Should get a value from the cache as bytes", func() { + value := "test" + + Expect(cache.Set("test", value, time.Minute)).To(BeNil()) + + newValue, err := cache.GetBytes("test") + + Expect(err).To(BeNil()) + Expect(newValue).To(Equal([]byte(value))) + }) + It("Should check if the cache has a value", func() { + value := "test" + + Expect(cache.Set("test", value, time.Minute)).To(BeNil()) + + Expect(cache.Has("test")).To(BeTrue()) + }) + It("Should delete a value from the cache", func() { + value := "test" + + Expect(cache.Set("test", value, time.Minute)).To(BeNil()) + + Expect(cache.Has("test")).To(BeTrue()) + + Expect(cache.Del("test")).To(BeNil()) + + Expect(cache.Has("test")).To(BeFalse()) + }) + }) +}) diff --git a/driver/memcache/memcache.go b/driver/memcache/memcache.go new file mode 100644 index 0000000..d6b3192 --- /dev/null +++ b/driver/memcache/memcache.go @@ -0,0 +1,68 @@ +package memcache + +import ( + "github.com/bradfitz/gomemcache/memcache" + "meow.tf/go/cacheinterface/v2/encoder" + "time" +) + +type Options struct { + Encoder encoder.Encoder `query:"encoder" default:"msgpack"` + Servers []string `default:"127.0.0.1:11211"` +} + +type Cache struct { + options Options + servers []string + client *memcache.Client +} + +func New(options Options) (*Cache, error) { + client := memcache.New(options.Servers...) + + return &Cache{ + options: options, + servers: options.Servers, + client: client, + }, nil +} + +func (mc *Cache) Has(key string) bool { + _, err := mc.client.Get(key) + + return err != nil +} + +func (mc *Cache) Get(key string, dst any) error { + item, err := mc.client.Get(key) + + if err != nil { + return err + } + + return encoder.DecodeValue(mc.options.Encoder, item.Value, dst) +} + +func (mc *Cache) GetBytes(key string) ([]byte, error) { + item, err := mc.client.Get(key) + + if err != nil { + return nil, err + } + + return item.Value, nil +} + +func (mc *Cache) Set(key string, val any, ttl time.Duration) error { + v, err := encoder.EncodeValue(mc.options.Encoder, val) + + if err != nil { + return err + } + + return mc.client.Set(&memcache.Item{Key: key, Value: v, Expiration: int32(ttl.Seconds())}) +} + +func (mc *Cache) Del(key string) error { + return mc.client.Delete(key) +} diff --git a/driver/memory/memory.go b/driver/memory/memory.go new file mode 100644 index 0000000..39be485 --- /dev/null +++ b/driver/memory/memory.go @@ -0,0 +1,199 @@ +package memory + +import ( + "errors" + "github.com/patrickmn/go-cache" + "meow.tf/go/cacheinterface/v2/encoder" + "reflect" + "time" +) + +var ( + ErrNotExist = errors.New("item does not exist") +) + +type Options struct { + Encoder encoder.Encoder `query:"encoder" default:"msgpack"` + DefaultExpiration time.Duration `query:"defaultExpiration"` + CleanupTime time.Duration `query:"cleanupTime" default:"5m"` +} + +type Cache struct { + options Options + c *cache.Cache +} + +func New(options Options) (*Cache, error) { + c := cache.New(1*time.Minute, options.CleanupTime) + + return &Cache{ + options: options, + c: c, + }, nil +} + +func (mc *Cache) Has(key string) bool { + _, exists := mc.c.Get(key) + + return exists +} + +func (mc *Cache) Get(key string, dst any) error { + item, exists := mc.c.Get(key) + + if !exists { + return ErrNotExist + } + + return CacheGet(item, dst) +} + +func (mc *Cache) GetBytes(key string) ([]byte, error) { + item, exists := mc.c.Get(key) + + if !exists { + return nil, ErrNotExist + } + + return CacheGetBytes(mc.options.Encoder, item) +} + +func (mc *Cache) Set(key string, val any, ttl time.Duration) error { + mc.c.Set(key, val, ttl) + return nil +} + +func (mc *Cache) Del(key string) error { + mc.c.Delete(key) + return nil +} + +func CacheGetBytes(encoder encoder.Encoder, item any) ([]byte, error) { + switch item.(type) { + case string: + return []byte(item.(string)), nil + case []byte: + return item.([]byte), nil + } + + return encoder.Marshal(item) +} + +func CacheGet(item any, v any) error { + switch v := v.(type) { + case *string: + if v != nil { + *v = item.(string) + return nil + } + case *[]byte: + if v != nil { + *v = item.([]byte) + return nil + } + case *int: + if v != nil { + *v = item.(int) + return nil + } + case *int8: + if v != nil { + *v = item.(int8) + return nil + } + case *int16: + if v != nil { + *v = item.(int16) + return nil + } + case *int32: + if v != nil { + *v = item.(int32) + return nil + } + case *int64: + if v != nil { + *v = item.(int64) + return nil + } + case *uint: + if v != nil { + *v = item.(uint) + return nil + } + case *uint8: + if v != nil { + *v = item.(uint8) + return nil + } + case *uint16: + if v != nil { + *v = item.(uint16) + return nil + } + case *uint32: + if v != nil { + *v = item.(uint32) + return nil + } + case *uint64: + if v != nil { + *v = item.(uint64) + return nil + } + case *bool: + if v != nil { + *v = item.(bool) + return nil + } + case *float32: + if v != nil { + *v = item.(float32) + return nil + } + case *float64: + if v != nil { + *v = item.(float64) + return nil + } + case *[]string: + *v = item.([]string) + return nil + case *map[string]string: + *v = item.(map[string]string) + return nil + case *map[string]any: + *v = item.(map[string]any) + return nil + case *time.Duration: + if v != nil { + *v = item.(time.Duration) + return nil + } + case *time.Time: + if v != nil { + *v = item.(time.Time) + return nil + } + } + + vv := reflect.ValueOf(v) + + if !vv.IsValid() { + return errors.New("dst pointer is not valid") + } + + if vv.Kind() != reflect.Ptr { + return errors.New("dst pointer is not a pointer") + } + + vv = vv.Elem() + + if !vv.IsValid() { + return errors.New("dst pointer is not a valid element") + } + + vv.Set(reflect.ValueOf(item)) + + return nil +} diff --git a/driver/memory/memory_suite_test.go b/driver/memory/memory_suite_test.go new file mode 100644 index 0000000..4e6884f --- /dev/null +++ b/driver/memory/memory_suite_test.go @@ -0,0 +1,13 @@ +package memory_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestMemory(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Memory Suite") +} diff --git a/driver/memory/memory_test.go b/driver/memory/memory_test.go new file mode 100644 index 0000000..85f6c91 --- /dev/null +++ b/driver/memory/memory_test.go @@ -0,0 +1,65 @@ +package memory + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "meow.tf/go/cacheinterface/v2/encoder" + "time" +) + +var _ = Describe("Memory driver", func() { + Context("Basic operations", func() { + var ( + cache *Cache + ) + BeforeEach(func() { + var err error + + cache, err = New(Options{ + Encoder: encoder.JSON, + }) + + Expect(err).To(BeNil()) + }) + It("Should store a value in the cache", func() { + value := "test" + Expect(cache.Set("test", value, time.Minute)).To(BeNil()) + }) + It("Should get a value from the cache", func() { + value := "test" + var newValue string + + Expect(cache.Set("test", value, time.Minute)).To(BeNil()) + Expect(cache.Get("test", &newValue)).To(BeNil()) + Expect(newValue).To(Equal(value)) + }) + It("Should get a value from the cache as bytes", func() { + value := "test" + + Expect(cache.Set("test", value, time.Minute)).To(BeNil()) + + newValue, err := cache.GetBytes("test") + + Expect(err).To(BeNil()) + Expect(newValue).To(Equal([]byte(value))) + }) + It("Should check if the cache has a value", func() { + value := "test" + + Expect(cache.Set("test", value, time.Minute)).To(BeNil()) + + Expect(cache.Has("test")).To(BeTrue()) + }) + It("Should delete a value from the cache", func() { + value := "test" + + Expect(cache.Set("test", value, time.Minute)).To(BeNil()) + + Expect(cache.Has("test")).To(BeTrue()) + + Expect(cache.Del("test")).To(BeNil()) + + Expect(cache.Has("test")).To(BeFalse()) + }) + }) +}) diff --git a/driver/redis/redis.go b/driver/redis/redis.go new file mode 100644 index 0000000..4164261 --- /dev/null +++ b/driver/redis/redis.go @@ -0,0 +1,73 @@ +package redis + +import ( + "github.com/hoisie/redis" + "meow.tf/go/cacheinterface/v2/encoder" + "time" +) + +type Options struct { + Encoder encoder.Encoder `query:"encoder" default:"msgpack"` + Address string `default:"127.0.0.1"` + DB int `default:"0" query:"db"` + Password string `query:"password"` +} + +type Cache struct { + options Options + client *redis.Client +} + +func New(options Options) (*Cache, error) { + rc := &redis.Client{ + Addr: options.Address, + Db: options.DB, + Password: options.Password, + } + + return &Cache{ + options: options, + client: rc, + }, nil +} + +func (rc *Cache) Has(key string) bool { + b, _ := rc.client.Exists(key) + return b +} + +func (rc *Cache) Get(key string, dst any) error { + b, err := rc.client.Get(key) + + if err != nil { + return err + } + + return encoder.DecodeValue(rc.options.Encoder, b, dst) +} + +func (rc *Cache) GetBytes(key string) ([]byte, error) { + b, err := rc.client.Get(key) + + if err != nil { + return nil, err + } + + return b, nil +} + +func (rc *Cache) Set(key string, val any, ttl time.Duration) error { + v, err := encoder.EncodeValue(rc.options.Encoder, val) + + if err != nil { + return err + } + + return rc.client.Setex(key, int64(ttl.Seconds()), v) +} + +func (rc *Cache) Del(key string) error { + _, err := rc.client.Del(key) + + return err +} diff --git a/encoder/binary.go b/encoder/binary.go new file mode 100644 index 0000000..1703ba3 --- /dev/null +++ b/encoder/binary.go @@ -0,0 +1,32 @@ +package encoder + +func EncodeValue(encoder Encoder, val any) ([]byte, error) { + var v []byte + + if b, ok := val.([]byte); ok { + v = b + } else if s, ok := val.(string); ok { + b = []byte(s) + } else { + return encoder.Marshal(val) + } + + return v, nil +} + +func DecodeValue(encoder Encoder, b []byte, v any) error { + switch v := v.(type) { + case *[]byte: + if v != nil { + *v = b + return nil + } + case *string: + if v != nil { + *v = string(b) + return nil + } + } + + return encoder.Unmarshal(b, v) +} diff --git a/encoder/encoding.go b/encoder/encoding.go new file mode 100644 index 0000000..70f13a6 --- /dev/null +++ b/encoder/encoding.go @@ -0,0 +1,66 @@ +package encoder + +import ( + "encoding/json" + "encoding/xml" + "github.com/vmihailenco/msgpack/v4" +) + +var ( + XML xmlEncoder + JSON jsonEncoder + MsgPack msgPackEncoder +) + +func EncoderFrom(name string) Encoder { + var enc Encoder + + switch name { + case "json": + enc = JSON + case "xml": + enc = XML + default: // Default is also msgpack + enc = MsgPack + } + + return enc +} + +type Encoder interface { + Unmarshal(b []byte, dest any) error + Marshal(value any) ([]byte, error) +} + +type jsonEncoder struct { +} + +func (j jsonEncoder) Marshal(value any) ([]byte, error) { + return json.Marshal(value) +} + +func (j jsonEncoder) Unmarshal(b []byte, dest any) error { + return json.Unmarshal(b, dest) +} + +type msgPackEncoder struct { +} + +func (m msgPackEncoder) Marshal(value any) ([]byte, error) { + return msgpack.Marshal(value) +} + +func (m msgPackEncoder) Unmarshal(b []byte, dest any) error { + return msgpack.Unmarshal(b, dest) +} + +type xmlEncoder struct { +} + +func (x xmlEncoder) Marshal(value any) ([]byte, error) { + return xml.Marshal(value) +} + +func (x xmlEncoder) Unmarshal(b []byte, dest any) error { + return xml.Unmarshal(b, dest) +} diff --git a/go.mod b/go.mod index f779336..f0e452d 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ -module meow.tf/go/cacheinterface +module meow.tf/go/cacheinterface/v2 -go 1.12 +go 1.18 require ( github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b @@ -9,3 +9,24 @@ require ( github.com/patrickmn/go-cache v2.1.0+incompatible github.com/vmihailenco/msgpack/v4 v4.2.0 ) + +require ( + github.com/go-logr/logr v1.2.3 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/vmihailenco/tagparser v0.1.0 // indirect + golang.org/x/net v0.5.0 // indirect + golang.org/x/sys v0.4.0 // indirect + golang.org/x/text v0.6.0 // indirect + google.golang.org/appengine v1.6.1 // indirect + google.golang.org/protobuf v1.28.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +require ( + github.com/iancoleman/strcase v0.2.0 + github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + github.com/onsi/ginkgo/v2 v2.8.0 + github.com/onsi/gomega v1.26.0 + gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect +) diff --git a/go.sum b/go.sum index 192a3c7..93da8ae 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,31 @@ 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/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 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/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk= github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 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/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= 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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/onsi/ginkgo/v2 v2.8.0 h1:pAM+oBNPrpXRs+E/8spkeGx9QgekbRVyr74EUvRVOUI= +github.com/onsi/ginkgo/v2 v2.8.0/go.mod h1:6JsQiECmxCa3V5st74AL/AmsV482EDdVrGaVW6z3oYU= +github.com/onsi/gomega v1.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q= +github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= 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= @@ -23,17 +37,31 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U 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/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= 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/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 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= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/lru.go b/lru.go deleted file mode 100644 index 4bf0bbc..0000000 --- a/lru.go +++ /dev/null @@ -1,46 +0,0 @@ -package cache - -import ( - "github.com/hashicorp/golang-lru" - "time" -) - -type LruCache struct { - c *lru.Cache -} - -func NewLruCache(size int) (CacheInterface, error) { - c, err := lru.New(size) - - if err != nil { - return nil, err - } - - return &LruCache{c: c}, nil -} - -func (mc *LruCache) Has(key string) bool { - _, exists := mc.c.Get(key) - - return exists -} - -func (mc *LruCache) Get(key string, dst ...interface{}) ([]byte, error) { - item, exists := mc.c.Get(key) - - if !exists { - return nil, ErrMemoryCacheNotExists - } - - return memoryCacheGet(item, dst...) -} - -func (mc *LruCache) Set(key string, val interface{}, ttl time.Duration) error { - mc.c.Add(key, val) - return nil -} - -func (mc *LruCache) Del(key string) error { - mc.c.Remove(key) - return nil -} diff --git a/lru_test.go b/lru_test.go deleted file mode 100644 index 3ebbf63..0000000 --- a/lru_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package cache - -import ( - "testing" - "time" -) - -func TestNew_LruURI(t *testing.T) { - cache, err := New("lru://") - - if err != nil { - t.Fatal("Error creating cache:", err) - } - - if _, ok := cache.(*LruCache); !ok { - t.Fatal("Cache is not instance of MemoryCache") - } -} - -func TestLruCache_Get(t *testing.T) { - cache, err := New("lru://") - - 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) - } -} - -func TestLruCache_GetRaw(t *testing.T) { - cache, err := New("lru://") - - if err != nil { - t.Fatal("Error creating cache:", err) - } - - obj := "test" - - cache.Set("test", obj, time.Minute) - - v, err := cache.Get("test") - - if err != nil { - t.Fatal("Unable to get value:", err) - } - - new := string(v) - - if obj != new { - t.Fatal("Expected", obj, "got", new) - } -} - -func TestLruCache_Has(t *testing.T) { - cache, err := New("lru://") - - if err != nil { - t.Fatal("Error creating cache:", err) - } - - cache.Set("test", "test", time.Minute) - - if !cache.Has("test") { - t.Fatal("Expected cache to have object 'test'") - } -} - -func TestLruCache_Del(t *testing.T) { - cache, err := New("lru://") - - if err != nil { - t.Fatal("Error creating cache:", err) - } - - cache.Set("test", "test", time.Minute) - - if !cache.Has("test") { - t.Fatal("Expected cache to have object 'test'") - } - - cache.Del("test") - - if cache.Has("test") { - t.Fatal("Cache did not properly delete item") - } -} diff --git a/memcache.go b/memcache.go deleted file mode 100644 index b7c188a..0000000 --- a/memcache.go +++ /dev/null @@ -1,58 +0,0 @@ -package cache - -import ( - "github.com/bradfitz/gomemcache/memcache" - "time" -) - -type MemcacheSettings struct { - Servers []string -} - -type MemcacheCache struct { - servers []string - backend *memcache.Client -} - -func NewMemcacheCache(s MemcacheSettings) (CacheInterface, error) { - c := memcache.New(s.Servers...) - - return &MemcacheCache{ - servers: s.Servers, - backend: c, - }, nil -} - -func (mc *MemcacheCache) Has(key string) bool { - _, err := mc.backend.Get(key) - - return err != nil -} - -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 { - v, err := encodeValue(val) - - if err != nil { - return err - } - - return mc.backend.Set(&memcache.Item{Key: key, Value: v, Expiration: int32(ttl.Seconds())}) -} - -func (mc *MemcacheCache) Del(key string) error { - return mc.backend.Delete(key) -} diff --git a/memcache_test.go b/memcache_test.go deleted file mode 100644 index a766c65..0000000 --- a/memcache_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package cache - -import ( - "testing" -) - -func Test_MemcacheURI(t *testing.T) { - cache, err := New("memcache://localhost") - - if err != nil { - t.Fatal("Error creating cache:", err) - } - - if _, ok := cache.(*MemcacheCache); !ok { - t.Fatal("Cache is not instance of MemcacheCache") - } -} - -func Test_MemcacheURIMultipleServers(t *testing.T) { - cache, err := New("memcache://localhost,localhost2") - - if err != nil { - t.Fatal("Error creating cache:", err) - } - - c, ok := cache.(*MemcacheCache) - - if !ok { - t.Fatal("Cache is not instance of MemcacheCache") - } - - if len(c.servers) != 2 { - t.Fatal("Number of servers does not match!") - } -} diff --git a/memory.go b/memory.go deleted file mode 100644 index 195c1fb..0000000 --- a/memory.go +++ /dev/null @@ -1,181 +0,0 @@ -package cache - -import ( - "errors" - "github.com/patrickmn/go-cache" - "github.com/vmihailenco/msgpack/v4" - "reflect" - "time" -) - -var ( - ErrMemoryCacheNotExists = 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, ErrMemoryCacheNotExists - } - - return memoryCacheGet(item, dst...) -} - -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 -} - -func memoryCacheGet(item interface{}, dst ...interface{}) ([]byte, error) { - if len(dst) == 0 { - switch item.(type) { - case string: - return []byte(item.(string)), nil - case []byte: - return item.([]byte), nil - } - - return msgpack.Marshal(item) - } - - v := dst[0] - - switch v := v.(type) { - case *string: - if v != nil { - *v = item.(string) - return []byte(item.(string)), nil - } - case *[]byte: - if v != nil { - *v = item.([]byte) - return item.([]byte), 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 -} diff --git a/memory_test.go b/memory_test.go deleted file mode 100644 index 34c3c66..0000000 --- a/memory_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package cache - -import ( - "testing" - "time" -) - -func TestNew_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 TestMemoryCache_Get(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) - } -} - -func TestMemoryCache_GetRaw(t *testing.T) { - cache, err := New("memory://") - - if err != nil { - t.Fatal("Error creating cache:", err) - } - - obj := "test" - - cache.Set("test", obj, time.Minute) - - v, err := cache.Get("test") - - if err != nil { - t.Fatal("Unable to get value:", err) - } - - new := string(v) - - if obj != new { - t.Fatal("Expected", obj, "got", new) - } -} - -func TestMemoryCache_Has(t *testing.T) { - cache, err := New("memory://") - - if err != nil { - t.Fatal("Error creating cache:", err) - } - - cache.Set("test", "test", time.Minute) - - if !cache.Has("test") { - t.Fatal("Expected cache to have object 'test'") - } -} - -func TestMemoryCache_Del(t *testing.T) { - cache, err := New("memory://") - - if err != nil { - t.Fatal("Error creating cache:", err) - } - - cache.Set("test", "test", time.Minute) - - if !cache.Has("test") { - t.Fatal("Expected cache to have object 'test'") - } - - cache.Del("test") - - if cache.Has("test") { - t.Fatal("Cache did not properly delete item") - } -} diff --git a/options.go b/options.go new file mode 100644 index 0000000..f986b61 --- /dev/null +++ b/options.go @@ -0,0 +1,169 @@ +package cache + +import ( + "errors" + "github.com/iancoleman/strcase" + "meow.tf/go/cacheinterface/v2/encoder" + "net/url" + "reflect" + "strconv" + "strings" + "time" +) + +func decodeQuery(query url.Values, dest any) error { + destType := reflect.TypeOf(dest) + + if destType.Kind() == reflect.Ptr { + destType = destType.Elem() + } + + destVal := reflect.ValueOf(dest) + + if destVal.Kind() == reflect.Ptr { + destVal = destVal.Elem() + } + + for i := 0; i < destType.NumField(); i++ { + fieldType := destType.Field(i) + field := destVal.Field(i) + + queryKeys := []string{ + fieldType.Tag.Get("query"), + strcase.ToLowerCamel(fieldType.Name), + } + + for _, key := range queryKeys { + if key == "" || !query.Has(key) { + continue + } + + // Map query variable to default field name + val, err := decodeType(fieldType.Type, query[key], false) + + if err != nil { + return err + } + + field.Set(reflect.ValueOf(val)) + + break + } + + // TODO: ignoring bool might not be the best choice. + // Maybe use bools as either pointers, or setup a "NullBool" like sql.Null*? + canHaveDefault := field.IsZero() && field.Kind() != reflect.Bool + + if def := fieldType.Tag.Get("default"); canHaveDefault && def != "" { + val, err := decodeType(fieldType.Type, []string{def}, true) + + if err != nil { + return err + } + + field.Set(reflect.ValueOf(val)) + } + } + + return nil +} + +var ( + encoderType = reflect.TypeOf((*encoder.Encoder)(nil)).Elem() + durationType = reflect.TypeOf(time.Duration(0)) +) + +func decodeType(t reflect.Type, val []string, isDefault bool) (any, error) { + switch t { + case encoderType: + return encoder.EncoderFrom(val[0]), nil + case durationType: + return time.ParseDuration(val[0]) + } + + switch t.Kind() { + case reflect.Bool: + v, err := strconv.ParseBool(val[0]) + + if err != nil { + return nil, err + } + + return v, nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + v, err := strconv.ParseInt(val[0], 10, 64) + + if err != nil { + return nil, err + } + + switch t.Kind() { + case reflect.Int: + return int(v), nil + case reflect.Int8: + return int8(v), nil + case reflect.Int16: + return int16(v), nil + case reflect.Int32: + return int32(v), nil + case reflect.Int64: + return v, nil + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + v, err := strconv.ParseUint(val[0], 10, 64) + + if err != nil { + return nil, err + } + + switch t.Kind() { + case reflect.Uint: + return uint(v), nil + case reflect.Uint8: + return uint8(v), nil + case reflect.Uint16: + return uint16(v), nil + case reflect.Uint32: + return uint32(v), nil + case reflect.Uint64: + return v, nil + } + case reflect.Float32, reflect.Float64: + v, err := strconv.ParseFloat(val[0], 64) + + if err != nil { + return nil, err + } + + switch t.Kind() { + case reflect.Float32: + return float32(v), nil + case reflect.Float64: + return v, nil + } + case reflect.String: + return val[0], nil + case reflect.Slice: + elemType := t.Elem() + + if isDefault { + val = strings.Split(val[0], ",") + } + + out := reflect.MakeSlice(t, 0, len(val)) + + for _, v := range val { + decodedVal, err := decodeType(elemType, []string{v}, isDefault) + + if err != nil { + return nil, err + } + + out = reflect.Append(out, reflect.ValueOf(decodedVal)) + } + + return out.Interface(), nil + } + + return nil, errors.New("unknown type " + t.String()) +} diff --git a/options_test.go b/options_test.go new file mode 100644 index 0000000..824787e --- /dev/null +++ b/options_test.go @@ -0,0 +1,95 @@ +package cache + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "meow.tf/go/cacheinterface/v2/encoder" + "net/url" + "time" +) + +var _ = Describe("Cache Test", func() { + Context("Query parsing", func() { + type testOpts struct { + Encoder encoder.Encoder + String string + Slice []string + Duration time.Duration + } + + var ( + opts testOpts + v url.Values + ) + BeforeEach(func() { + opts = testOpts{} + v = url.Values{} + }) + It("Should parse custom interfaces", func() { + v.Set("encoder", "json") + + err := decodeQuery(v, &opts) + + Expect(err).To(BeNil()) + Expect(opts.Encoder).To(Equal(encoder.JSON)) + }) + It("Should parse strings", func() { + v.Set("string", "testing") + + err := decodeQuery(v, &opts) + + Expect(err).To(BeNil()) + Expect(opts.String).To(Equal("testing")) + }) + It("Should parse slices", func() { + v["slice"] = []string{"bla", "bla"} + + err := decodeQuery(v, &opts) + + Expect(err).To(BeNil()) + Expect(opts.Slice).To(Equal([]string{"bla", "bla"})) + }) + It("Should parse durations", func() { + v.Set("duration", "5m") + + err := decodeQuery(v, &opts) + + Expect(err).To(BeNil()) + Expect(opts.Duration).To(Equal(5 * time.Minute)) + }) + }) + Context("Defaults", func() { + type testOpts struct { + String string `default:"Test"` + Slice []string `default:"bla,bla"` + Duration time.Duration `default:"5m"` + Test bool `default:"true"` + } + var ( + opts testOpts + v url.Values + ) + BeforeEach(func() { + opts = testOpts{} + v = url.Values{} + }) + It("Should assign defaults to strings", func() { + err := decodeQuery(v, &opts) + + Expect(err).To(BeNil()) + Expect(opts.String).To(Equal("Test")) + }) + It("Should assign defaults to string slices", func() { + err := decodeQuery(v, &opts) + + Expect(err).To(BeNil()) + Expect(opts.Slice).To(Equal([]string{"bla", "bla"})) + }) + It("Should assign defaults to durations", func() { + err := decodeQuery(v, &opts) + + Expect(err).To(BeNil()) + Expect(opts.Duration).To(Equal(5 * time.Minute)) + }) + }) +}) diff --git a/redis.go b/redis.go deleted file mode 100644 index 4384eee..0000000 --- a/redis.go +++ /dev/null @@ -1,61 +0,0 @@ -package cache - -import ( - "github.com/hoisie/redis" - "time" -) - -type RedisSettings struct { - Address string - DB int - Password string -} - -type RedisCache struct { - CacheInterface - - c *redis.Client -} - -func NewRedisCache(c RedisSettings) (CacheInterface, error) { - rc := &redis.Client{Addr: c.Address, Db: c.DB, Password: c.Password} - - return &RedisCache{ - c: rc, - }, nil -} - -func (rc *RedisCache) Has(key string) bool { - b, _ := rc.c.Exists(key) - return b -} - -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 { - v, err := encodeValue(val) - - if err != nil { - return err - } - - return rc.c.Setex(key, int64(ttl.Seconds()), v) -} - -func (rc *RedisCache) Del(key string) error { - _, err := rc.c.Del(key) - - return err -} diff --git a/redis_test.go b/redis_test.go deleted file mode 100644 index f493f5f..0000000 --- a/redis_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package cache - -import ( - "testing" -) - -func Test_RedisURI(t *testing.T) { - cache, err := New("redis://127.0.0.1:6389") - - if err != nil { - t.Fatal("Error creating cache:", err) - } - - if _, ok := cache.(*RedisCache); !ok { - t.Fatal("Cache is not instance of RedisCache") - } -}