From 450d67cce8a6e2fa0d2a32796df00f638b23ac55 Mon Sep 17 00:00:00 2001 From: Alejandro Date: Fri, 16 Mar 2018 04:06:39 +0000 Subject: [PATCH 1/7] adding caching abtraction layer to utron framework. --- cache/cache.go | 32 +++++ cache/connector_test.go | 81 +++++++++++ cache/contracts.go | 56 ++++++++ cache/helpers.go | 84 +++++++++++ cache/map_connector.go | 34 +++++ cache/map_store.go | 179 +++++++++++++++++++++++ cache/memcache_connector.go | 51 +++++++ cache/memcache_store.go | 239 +++++++++++++++++++++++++++++++ cache/redis_connector.go | 51 +++++++ cache/redis_store.go | 228 +++++++++++++++++++++++++++++ cache/redis_tagged_cache.go | 113 +++++++++++++++ cache/store_test.go | 276 ++++++++++++++++++++++++++++++++++++ cache/tag_set.go | 80 +++++++++++ cache/tagged_cache.go | 173 ++++++++++++++++++++++ cache/tagged_cache_test.go | 193 +++++++++++++++++++++++++ 15 files changed, 1870 insertions(+) create mode 100644 cache/cache.go create mode 100644 cache/connector_test.go create mode 100644 cache/contracts.go create mode 100644 cache/helpers.go create mode 100644 cache/map_connector.go create mode 100644 cache/map_store.go create mode 100644 cache/memcache_connector.go create mode 100644 cache/memcache_store.go create mode 100644 cache/redis_connector.go create mode 100644 cache/redis_store.go create mode 100644 cache/redis_tagged_cache.go create mode 100644 cache/store_test.go create mode 100644 cache/tag_set.go create mode 100644 cache/tagged_cache.go create mode 100644 cache/tagged_cache_test.go diff --git a/cache/cache.go b/cache/cache.go new file mode 100644 index 0000000..1d21736 --- /dev/null +++ b/cache/cache.go @@ -0,0 +1,32 @@ +package cache + +import ( + "strings" +) + +// REDIS_DRIVER specifies the redis driver name +const REDIS_DRIVER = "redis" + +// MEMCACHE_DRIVER specifies the memcache driver name +const MEMCACHE_DRIVER = "memcache" + +// MAP_DRIVER specifies the map driver name +const MAP_DRIVER = "map" + +// New new-ups an instance of StoreInterface +func New(driver string, params map[string]interface{}) (StoreInterface, error) { + switch strings.ToLower(driver) { + case REDIS_DRIVER: + return connect(new(RedisConnector), params) + case MEMCACHE_DRIVER: + return connect(new(MemcacheConnector), params) + case MAP_DRIVER: + return connect(new(MapConnector), params) + } + + return connect(new(MapConnector), params) +} + +func connect(connector CacheConnectorInterface, params map[string]interface{}) (StoreInterface, error) { + return connector.Connect(params) +} diff --git a/cache/connector_test.go b/cache/connector_test.go new file mode 100644 index 0000000..37a1c86 --- /dev/null +++ b/cache/connector_test.go @@ -0,0 +1,81 @@ +package cache + +import ( + "testing" +) + +func TestMemcacheConnector(t *testing.T) { + memcacheConnector := new(MemcacheConnector) + + memcacheStore, err := memcacheConnector.Connect(memcacheStore()) + + if err != nil { + panic(err) + } + + _, ok := memcacheStore.(StoreInterface) + + if !ok { + t.Error("Expected StoreInterface got", memcacheStore) + } +} + +func TestRedisConnector(t *testing.T) { + redisConnector := new(RedisConnector) + + redisStore, err := redisConnector.Connect(redisStore()) + + if err != nil { + panic(err) + } + + _, ok := redisStore.(StoreInterface) + + if !ok { + t.Error("Expected StoreInterface got", redisStore) + } +} + +func TestArrayConnector(t *testing.T) { + mapConnector := new(MapConnector) + + mapStore, err := mapConnector.Connect(mapStore()) + + if err != nil { + panic(err) + } + + _, ok := mapStore.(StoreInterface) + + if !ok { + t.Error("Expected StoreInterface got", mapStore) + } +} + +func redisStore() map[string]interface{} { + params := make(map[string]interface{}) + + params["address"] = "localhost:6379" + params["password"] = "" + params["database"] = 0 + params["prefix"] = "golavel:" + + return params +} + +func memcacheStore() map[string]interface{} { + params := make(map[string]interface{}) + + params["server 1"] = "127.0.0.1:11211" + params["prefix"] = "golavel:" + + return params +} + +func mapStore() map[string]interface{} { + params := make(map[string]interface{}) + + params["prefix"] = "golavel:" + + return params +} diff --git a/cache/contracts.go b/cache/contracts.go new file mode 100644 index 0000000..ed5a0b2 --- /dev/null +++ b/cache/contracts.go @@ -0,0 +1,56 @@ +package cache + +// CacheConnectorInterface represents the connector methods to be implemented +type CacheConnectorInterface interface { + Connect(params map[string]interface{}) (StoreInterface, error) + + validate(params map[string]interface{}) (map[string]interface{}, error) +} + +// CacheInterface represents the caching methods to be implemented +type CacheInterface interface { + Get(key string) (interface{}, error) + + Put(key string, value interface{}, minutes int) error + + Increment(key string, value int64) (int64, error) + + Decrement(key string, value int64) (int64, error) + + Forget(key string) (bool, error) + + Forever(key string, value interface{}) error + + Flush() (bool, error) + + GetInt(key string) (int64, error) + + GetFloat(key string) (float64, error) + + GetPrefix() string + + Many(keys []string) (map[string]interface{}, error) + + PutMany(values map[string]interface{}, minutes int) error + + GetStruct(key string, entity interface{}) (interface{}, error) +} + +// TagsInterface represents the tagging methods to be implemented +type TagsInterface interface { + Tags(names ...string) TaggedStoreInterface +} + +// StoreInterface represents the methods a caching store needs to implement +type StoreInterface interface { + CacheInterface + + TagsInterface +} + +// TaggedStoreInterface represents the methods a tagged-caching store needs to implement +type TaggedStoreInterface interface { + CacheInterface + + TagFlush() error +} diff --git a/cache/helpers.go b/cache/helpers.go new file mode 100644 index 0000000..7ba8b0e --- /dev/null +++ b/cache/helpers.go @@ -0,0 +1,84 @@ +package cache + +import ( + "bytes" + "encoding/json" + "math" + "strconv" +) + +// Encode encodes json.Marshal to a string +func Encode(item interface{}) (string, error) { + value, err := json.Marshal(item) + + return string(value), err +} + +// SimpleDecode decodes json.Unmarshal to a string +func SimpleDecode(value string) (string, error) { + err := json.Unmarshal([]byte(value), &value) + + return string(value), err +} + +// Decode performs a json.Unmarshal +func Decode(value string, entity interface{}) (interface{}, error) { + err := json.Unmarshal([]byte(value), &entity) + + return entity, err +} + +// IsNumeric check if the provided value is numeric +func IsNumeric(s interface{}) bool { + switch s.(type) { + case int: + return true + case int32: + return true + case float32: + return true + case float64: + return true + default: + return false + } +} + +// GetTaggedManyKey returns a tagged many key +func GetTaggedManyKey(prefix string, key string) string { + count := len(prefix) + 41 + + sub := "" + subs := []string{} + + runs := bytes.Runes([]byte(key)) + + for i, run := range runs { + sub = sub + string(run) + if (i+1)%count == 0 { + subs = append(subs, sub) + sub = "" + } else if (i + 1) == len(runs) { + subs = append(subs, sub) + } + } + + return subs[1] +} + +// IsStringNumeric checks if a string is numeric +func IsStringNumeric(value string) bool { + _, err := strconv.ParseFloat(value, 64) + + return err == nil +} + +// StringToFloat64 converts a string to float64 +func StringToFloat64(value string) (float64, error) { + return strconv.ParseFloat(value, 64) +} + +// IsFloat checks if the provided value can be truncated to an int +func IsFloat(value float64) bool { + return value != math.Trunc(value) +} diff --git a/cache/map_connector.go b/cache/map_connector.go new file mode 100644 index 0000000..fd8e7b5 --- /dev/null +++ b/cache/map_connector.go @@ -0,0 +1,34 @@ +package cache + +import ( + "errors" +) + +// MapConnector is a representation of the array store connector +type MapConnector struct{} + +// Connect is responsible for connecting with the caching store +func (ac *MapConnector) Connect(params map[string]interface{}) (StoreInterface, error) { + params, err := ac.validate(params) + + if err != nil { + return &MapStore{}, err + } + + prefix := params["prefix"].(string) + + delete(params, "prefix") + + return &MapStore{ + Client: make(map[string]interface{}), + Prefix: prefix, + }, nil +} + +func (ac *MapConnector) validate(params map[string]interface{}) (map[string]interface{}, error) { + if _, ok := params["prefix"]; !ok { + return params, errors.New("You need to specify a caching prefix.") + } + + return params, nil +} diff --git a/cache/map_store.go b/cache/map_store.go new file mode 100644 index 0000000..d043d5f --- /dev/null +++ b/cache/map_store.go @@ -0,0 +1,179 @@ +package cache + +import ( + "errors" + "strconv" +) + +// MapStore is the representation of a map caching store +type MapStore struct { + Client map[string]interface{} + Prefix string +} + +// Get gets a value from the store +func (ms *MapStore) Get(key string) (interface{}, error) { + value := ms.Client[ms.GetPrefix()+key] + + if value == nil { + return "", nil + } + + if IsStringNumeric(value.(string)) { + floatValue, err := StringToFloat64(value.(string)) + + if err != nil { + return floatValue, err + } + + if IsFloat(floatValue) { + return floatValue, err + } + + return int64(floatValue), err + } + + return SimpleDecode(value.(string)) +} + +// GetFloat gets a float value from the store +func (ms *MapStore) GetFloat(key string) (float64, error) { + value := ms.Client[ms.GetPrefix()+key] + + if value == nil || !IsStringNumeric(value.(string)) { + return 0, errors.New("Invalid numeric value") + } + + return StringToFloat64(value.(string)) +} + +// GetInt gets an int value from the store +func (ms *MapStore) GetInt(key string) (int64, error) { + value := ms.Client[ms.GetPrefix()+key] + + if value == nil || !IsStringNumeric(value.(string)) { + return 0, errors.New("Invalid numeric value") + } + + val, err := StringToFloat64(value.(string)) + + return int64(val), err +} + +// Increment increments an integer counter by a given value +func (ms *MapStore) Increment(key string, value int64) (int64, error) { + val := ms.Client[ms.GetPrefix()+key] + + if val != nil { + if IsStringNumeric(val.(string)) { + floatValue, err := StringToFloat64(val.(string)) + + if err != nil { + return 0, err + } + + result := value + int64(floatValue) + + err = ms.Put(key, result, 0) + + return result, err + } + + } + + err := ms.Put(key, value, 0) + + return value, err +} + +// Decrement decrements an integer counter by a given value +func (ms *MapStore) Decrement(key string, value int64) (int64, error) { + return ms.Increment(key, -value) +} + +// Put puts a value in the given store for a predetermined amount of time in mins. +func (ms *MapStore) Put(key string, value interface{}, minutes int) error { + val, err := Encode(value) + + mins := strconv.Itoa(minutes) + + mins = "" + + ms.Client[ms.GetPrefix()+key+mins] = val + + return err +} + +// Forever puts a value in the given store until it is forgotten/evicted +func (ms *MapStore) Forever(key string, value interface{}) error { + return ms.Put(key, value, 0) +} + +// Flush flushes the store +func (ms *MapStore) Flush() (bool, error) { + ms.Client = make(map[string]interface{}) + + return true, nil +} + +// Forget forgets/evicts a given key-value pair from the store +func (ms *MapStore) Forget(key string) (bool, error) { + _, ok := ms.Client[ms.GetPrefix()+key] + + if ok { + delete(ms.Client, ms.GetPrefix()+key) + + return true, nil + } + + return false, nil +} + +// GetPrefix gets the cache key prefix +func (ms *MapStore) GetPrefix() string { + return ms.Prefix +} + +// PutMany puts many values in the given store until they are forgotten/evicted +func (ms *MapStore) PutMany(values map[string]interface{}, minutes int) error { + for key, value := range values { + ms.Put(key, value, minutes) + } + + return nil +} + +// Many gets many values from the store +func (ms *MapStore) Many(keys []string) (map[string]interface{}, error) { + items := make(map[string]interface{}) + + for _, key := range keys { + val, err := ms.Get(key) + + if err != nil { + return items, err + } + + items[key] = val + } + + return items, nil +} + +// GetStruct gets the struct representation of a value from the store +func (ms *MapStore) GetStruct(key string, entity interface{}) (interface{}, error) { + value := ms.Client[ms.GetPrefix()+key] + + return Decode(value.(string), entity) +} + +// Tags returns the TaggedCache for the given store +func (ms *MapStore) Tags(names ...string) TaggedStoreInterface { + return &TaggedCache{ + Store: ms, + Tags: TagSet{ + Store: ms, + Names: names, + }, + } +} diff --git a/cache/memcache_connector.go b/cache/memcache_connector.go new file mode 100644 index 0000000..da9368a --- /dev/null +++ b/cache/memcache_connector.go @@ -0,0 +1,51 @@ +package cache + +import ( + "errors" + "github.com/bradfitz/gomemcache/memcache" +) + +// MemcacheConnector is the representation of the memcache store connector +type MemcacheConnector struct{} + +// Connect is responsible for connecting with the caching store +func (mc *MemcacheConnector) Connect(params map[string]interface{}) (StoreInterface, error) { + params, err := mc.validate(params) + + if err != nil { + return &MemcacheStore{}, err + } + + prefix := params["prefix"].(string) + + delete(params, "prefix") + + return &MemcacheStore{ + Client: mc.client(params), + Prefix: prefix, + }, nil +} + +func (mc *MemcacheConnector) client(params map[string]interface{}) memcache.Client { + servers := make([]string, len(params)-1) + + for _, param := range params { + servers = append(servers, param.(string)) + } + + return *memcache.New(servers...) +} + +func (mc *MemcacheConnector) validate(params map[string]interface{}) (map[string]interface{}, error) { + if _, ok := params["prefix"]; !ok { + return params, errors.New("You need to specify a caching prefix.") + } + + for key, param := range params { + if _, ok := param.(string); !ok { + return params, errors.New("The" + key + "parameter is not of type string.") + } + } + + return params, nil +} diff --git a/cache/memcache_store.go b/cache/memcache_store.go new file mode 100644 index 0000000..6bd69c8 --- /dev/null +++ b/cache/memcache_store.go @@ -0,0 +1,239 @@ +package cache + +import ( + "errors" + "github.com/bradfitz/gomemcache/memcache" +) + +// MEMCACHE_NIL_ERROR_RESPONSE is the gomemcache nil response error +const MEMCACHE_NIL_ERROR_RESPONSE = "memcache: cache miss" + +// MemcacheStore is the representation of the memcache caching store +type MemcacheStore struct { + Client memcache.Client + Prefix string +} + +// Put puts a value in the given store for a predetermined amount of time in mins. +func (ms *MemcacheStore) Put(key string, value interface{}, minutes int) error { + item, err := ms.item(key, value, minutes) + + if err != nil { + return err + } + + return ms.Client.Set(item) +} + +// Forever puts a value in the given store until it is forgotten/evicted +func (ms *MemcacheStore) Forever(key string, value interface{}) error { + return ms.Put(key, value, 0) +} + +// Get gets a value from the store +func (ms *MemcacheStore) Get(key string) (interface{}, error) { + value, err := ms.get(key) + + if err != nil { + return value, err + } + + return ms.processValue(value) +} + +// GetFloat gets a float value from the store +func (ms *MemcacheStore) GetFloat(key string) (float64, error) { + value, err := ms.get(key) + + if err != nil { + return 0.0, err + } + + if !IsStringNumeric(value) { + return 0.0, errors.New("Invalid numeric value") + } + + return StringToFloat64(value) +} + +// GetInt gets an int value from the store +func (ms *MemcacheStore) GetInt(key string) (int64, error) { + value, err := ms.get(key) + + if err != nil { + return 0, err + } + + if !IsStringNumeric(value) { + return 0, errors.New("Invalid numeric value") + } + + val, err := StringToFloat64(value) + + return int64(val), err +} + +// Increment increments an integer counter by a given value +func (ms *MemcacheStore) Increment(key string, value int64) (int64, error) { + newValue, err := ms.Client.Increment(ms.GetPrefix()+key, uint64(value)) + + if err != nil { + if err.Error() != "memcache: cache miss" { + return value, err + } + + ms.Put(key, value, 0) + + return value, nil + } + + return int64(newValue), nil +} + +// Decrement decrements an integer counter by a given value +func (ms *MemcacheStore) Decrement(key string, value int64) (int64, error) { + newValue, err := ms.Client.Decrement(ms.GetPrefix()+key, uint64(value)) + + if err != nil { + if err.Error() != "memcache: cache miss" { + return value, err + } + + ms.Put(key, 0, 0) + + return int64(0), nil + } + + return int64(newValue), nil +} + +// GetPrefix gets the cache key prefix +func (ms *MemcacheStore) GetPrefix() string { + return ms.Prefix +} + +// PutMany puts many values in the given store until they are forgotten/evicted +func (ms *MemcacheStore) PutMany(values map[string]interface{}, minutes int) error { + for key, value := range values { + err := ms.Put(key, value, minutes) + + if err != nil { + return err + } + } + + return nil +} + +// Many gets many values from the store +func (ms *MemcacheStore) Many(keys []string) (map[string]interface{}, error) { + items := make(map[string]interface{}) + + for _, key := range keys { + val, err := ms.Get(key) + + if err != nil { + return items, err + } + + items[key] = val + } + + return items, nil +} + +// Forget forgets/evicts a given key-value pair from the store +func (ms *MemcacheStore) Forget(key string) (bool, error) { + err := ms.Client.Delete(ms.GetPrefix() + key) + + if err != nil { + return false, err + } + + return true, nil +} + +// Flush flushes the store +func (ms *MemcacheStore) Flush() (bool, error) { + err := ms.Client.DeleteAll() + + if err != nil { + return false, err + } + + return true, nil +} + +// GetStruct gets the struct representation of a value from the store +func (ms *MemcacheStore) GetStruct(key string, entity interface{}) (interface{}, error) { + value, err := ms.get(key) + + if err != nil { + return value, err + } + + return Decode(value, entity) +} + +// Tags returns the TaggedCache for the given store +func (ms *MemcacheStore) Tags(names ...string) TaggedStoreInterface { + return &TaggedCache{ + Store: ms, + Tags: TagSet{ + Store: ms, + Names: names, + }, + } +} + +func (ms *MemcacheStore) get(key string) (string, error) { + item, err := ms.Client.Get(ms.GetPrefix() + key) + + if err != nil { + if err.Error() == MEMCACHE_NIL_ERROR_RESPONSE { + return "", nil + } + + return "", err + } + + return ms.getItemValue(item.Value), nil +} + +func (ms *MemcacheStore) getItemValue(itemValue []byte) string { + value, err := SimpleDecode(string(itemValue)) + + if err != nil { + return string(itemValue) + } + + return value +} + +func (ms *MemcacheStore) processValue(value string) (interface{}, error) { + if IsStringNumeric(value) { + floatValue, err := StringToFloat64(value) + + if err != nil { + return floatValue, err + } + + if IsFloat(floatValue) { + return floatValue, err + } + + return int64(floatValue), err + } + + return value, nil +} + +func (ms *MemcacheStore) item(key string, value interface{}, minutes int) (*memcache.Item, error) { + val, err := Encode(value) + + return &memcache.Item{ + Key: ms.GetPrefix() + key, + Value: []byte(val), + Expiration: int32(minutes), + }, err +} diff --git a/cache/redis_connector.go b/cache/redis_connector.go new file mode 100644 index 0000000..89af272 --- /dev/null +++ b/cache/redis_connector.go @@ -0,0 +1,51 @@ +package cache + +import ( + "errors" + "github.com/go-redis/redis" +) + +// RedisConnector is the representation of the redis store connector +type RedisConnector struct{} + +// Connect is responsible for connecting with the caching store +func (rc *RedisConnector) Connect(params map[string]interface{}) (StoreInterface, error) { + params, err := rc.validate(params) + + if err != nil { + return &RedisStore{}, err + } + + return &RedisStore{ + Client: rc.client(params["address"].(string), params["password"].(string), params["database"].(int)), + Prefix: params["prefix"].(string), + }, nil +} + +func (rc *RedisConnector) client(address string, password string, database int) redis.Client { + return *redis.NewClient(&redis.Options{ + Addr: address, + Password: password, + DB: database, + }) +} + +func (rc *RedisConnector) validate(params map[string]interface{}) (map[string]interface{}, error) { + if _, ok := params["address"]; !ok { + return params, errors.New("You need to specify an address for your redis server. Ex: localhost:6379") + } + + if _, ok := params["database"]; !ok { + return params, errors.New("You need to specify a database for your redis server. From 1 to 16 0-indexed") + } + + if _, ok := params["password"]; !ok { + return params, errors.New("You need to specify a password for your redis server.") + } + + if _, ok := params["prefix"]; !ok { + return params, errors.New("You need to specify a caching prefix.") + } + + return params, nil +} diff --git a/cache/redis_store.go b/cache/redis_store.go new file mode 100644 index 0000000..e522331 --- /dev/null +++ b/cache/redis_store.go @@ -0,0 +1,228 @@ +package cache + +import ( + "errors" + "github.com/go-redis/redis" + "strconv" + "time" +) + +// REDIS_NIL_ERROR_RESPONSE go-redis nil response error +const REDIS_NIL_ERROR_RESPONSE = "redis: nil" + +// RedisStore is the representation of the redis caching store +type RedisStore struct { + Client redis.Client + Prefix string +} + +// Get gets a value from the store +func (rs *RedisStore) Get(key string) (interface{}, error) { + intVal, err := rs.get(key).Int64() + + if err != nil { + floatVal, err := rs.get(key).Float64() + + if err != nil { + value, err := rs.get(key).Result() + + if err != nil { + if err.Error() == REDIS_NIL_ERROR_RESPONSE { + return "", nil + } + + return value, err + } + + return SimpleDecode(value) + } + + if &floatVal == nil { + return floatVal, errors.New("Float value is nil.") + } + + return floatVal, nil + } + + if &intVal == nil { + return intVal, errors.New("Int value is nil.") + } + + return intVal, nil +} + +// GetFloat gets a float value from the store +func (rs *RedisStore) GetFloat(key string) (float64, error) { + return rs.get(key).Float64() +} + +// GetInt gets an int value from the store +func (rs *RedisStore) GetInt(key string) (int64, error) { + return rs.get(key).Int64() +} + +// Increment increments an integer counter by a given value +func (rs *RedisStore) Increment(key string, value int64) (int64, error) { + return rs.Client.IncrBy(rs.Prefix+key, value).Result() +} + +// Decrement decrements an integer counter by a given value +func (rs *RedisStore) Decrement(key string, value int64) (int64, error) { + return rs.Client.DecrBy(rs.Prefix+key, value).Result() +} + +// Put puts a value in the given store for a predetermined amount of time in mins. +func (rs *RedisStore) Put(key string, value interface{}, minutes int) error { + time, err := time.ParseDuration(strconv.Itoa(minutes) + "m") + + if err != nil { + return err + } + + if IsNumeric(value) { + return rs.Client.Set(rs.Prefix+key, value, time).Err() + } + + val, err := Encode(value) + + if err != nil { + return err + } + + return rs.Client.Set(rs.GetPrefix()+key, val, time).Err() +} + +// Forever puts a value in the given store until it is forgotten/evicted +func (rs *RedisStore) Forever(key string, value interface{}) error { + if IsNumeric(value) { + err := rs.Client.Set(rs.Prefix+key, value, 0).Err() + + if err != nil { + return err + } + + return rs.Client.Persist(rs.Prefix + key).Err() + } + + val, err := Encode(value) + + if err != nil { + return err + } + + err = rs.Client.Set(rs.Prefix+key, val, 0).Err() + + if err != nil { + return err + } + + return rs.Client.Persist(rs.Prefix + key).Err() +} + +// Flush flushes the store +func (rs *RedisStore) Flush() (bool, error) { + err := rs.Client.FlushDB().Err() + + if err != nil { + return false, err + } + + return true, nil +} + +// Forget forgets/evicts a given key-value pair from the store +func (rs *RedisStore) Forget(key string) (bool, error) { + err := rs.Client.Del(rs.Prefix + key).Err() + + if err != nil { + return false, err + } + + return true, nil +} + +// GetPrefix gets the cache key prefix +func (rs *RedisStore) GetPrefix() string { + return rs.Prefix +} + +// PutMany puts many values in the given store until they are forgotten/evicted +func (rs *RedisStore) PutMany(values map[string]interface{}, minutes int) error { + pipe := rs.Client.TxPipeline() + + for key, value := range values { + err := rs.Put(key, value, minutes) + + if err != nil { + return err + } + } + + _, err := pipe.Exec() + + return err +} + +// Many gets many values from the store +func (rs *RedisStore) Many(keys []string) (map[string]interface{}, error) { + values := make(map[string]interface{}) + + pipe := rs.Client.TxPipeline() + + for _, key := range keys { + val, err := rs.Get(key) + + if err != nil { + return values, err + } + + values[key] = val + } + + _, err := pipe.Exec() + + return values, err +} + +// Connection returns the the store's client +func (rs *RedisStore) Connection() interface{} { + return rs.Client +} + +// Lpush runs the Redis lpush command +func (rs *RedisStore) Lpush(segment string, key string) { + rs.Client.LPush(segment, key) +} + +// Lrange runs the Redis lrange command +func (rs *RedisStore) Lrange(key string, start int64, stop int64) []string { + return rs.Client.LRange(key, start, stop).Val() +} + +// Tags returns the TaggedCache for the given store +func (rs *RedisStore) Tags(names ...string) TaggedStoreInterface { + return &RedisTaggedCache{ + TaggedCache{ + Store: rs, + Tags: TagSet{ + Store: rs, + Names: names, + }, + }, + } +} + +// GetStruct gets the struct representation of a value from the store +func (rs *RedisStore) GetStruct(key string, entity interface{}) (interface{}, error) { + value, err := rs.get(key).Result() + + if err != nil { + return value, err + } + + return Decode(value, entity) +} + +func (rs *RedisStore) get(key string) *redis.StringCmd { + return rs.Client.Get(rs.Prefix + key) +} diff --git a/cache/redis_tagged_cache.go b/cache/redis_tagged_cache.go new file mode 100644 index 0000000..14ea009 --- /dev/null +++ b/cache/redis_tagged_cache.go @@ -0,0 +1,113 @@ +package cache + +import ( + "crypto/sha1" + "encoding/hex" + "reflect" + "strings" +) + +// RedisTaggedCache is the representation of the redis tagged cache store +type RedisTaggedCache struct { + TaggedCache +} + +// Forever puts a value in the given store until it is forgotten/evicted +func (rtc *RedisTaggedCache) Forever(key string, value interface{}) error { + namespace, err := rtc.Tags.GetNamespace() + + if err != nil { + return err + } + + rtc.pushForever(namespace, key) + + h := sha1.New() + + h.Write(([]byte(namespace))) + + return rtc.Store.Forever(rtc.GetPrefix()+hex.EncodeToString(h.Sum(nil))+":"+key, value) +} + +// TagFlush flushes the tags of the TaggedCache +func (rtc *RedisTaggedCache) TagFlush() error { + return rtc.deleteForeverKeys() +} + +func (rtc *RedisTaggedCache) pushForever(namespace string, key string) { + h := sha1.New() + + h.Write(([]byte(namespace))) + + fullKey := rtc.GetPrefix() + hex.EncodeToString(h.Sum(nil)) + ":" + key + + segments := strings.Split(namespace, "|") + + for _, segment := range segments { + + inputs := []reflect.Value{ + reflect.ValueOf(rtc.foreverKey(segment)), + reflect.ValueOf(fullKey), + } + + reflect.ValueOf(rtc.Store).MethodByName("Lpush").Call(inputs) + } +} + +func (rtc *RedisTaggedCache) deleteForeverKeys() error { + namespace, err := rtc.Tags.GetNamespace() + + if err != nil { + return err + } + + segments := strings.Split(namespace, "|") + + for _, segment := range segments { + key := rtc.foreverKey(segment) + + err = rtc.deleteForeverValues(key) + + if err != nil { + return err + } + + _, err = rtc.Store.Forget(segment) + + if err != nil { + return err + } + } + + return nil +} + +func (rtc *RedisTaggedCache) deleteForeverValues(key string) error { + inputs := []reflect.Value{ + reflect.ValueOf(key), + reflect.ValueOf(int64(0)), + reflect.ValueOf(int64(-1)), + } + + keys := reflect.ValueOf(rtc.Store).MethodByName("Lrange").Call(inputs) + + if len(keys) > 0 { + for _, key := range keys { + if key.Len() > 0 { + for i := 0; i < key.Len(); i++ { + _, err := rtc.Store.Forget(key.Index(i).String()) + + if err != nil { + return err + } + } + } + } + } + + return nil +} + +func (rtc *RedisTaggedCache) foreverKey(segment string) string { + return rtc.GetPrefix() + segment + ":forever" +} diff --git a/cache/store_test.go b/cache/store_test.go new file mode 100644 index 0000000..9b7d48e --- /dev/null +++ b/cache/store_test.go @@ -0,0 +1,276 @@ +package cache + +import ( + "strings" + "testing" +) + +var drivers = []string{ + "map", + "memcache", + "redis", +} + +type Example struct { + Name string + Description string +} + +func TestPutGet(t *testing.T) { + for _, driver := range drivers { + cache := store(driver) + + cache.Put("key", "value", 1) + + got, err := cache.Get("key") + + if got != "value" || err != nil { + t.Error("Expected value, got ", got) + } + + cache.Put("key", 1, 1) + + got, err = cache.Get("key") + + if got != int64(1) || err != nil { + t.Error("Expected 1, got ", got) + } + + cache.Put("key", 2.99, 1) + + got, err = cache.Get("key") + + if got != float64(2.99) || err != nil { + t.Error("Expected 2.99, got", got) + } + + cache.Forget("key") + } +} + +func TestPutGetInt(t *testing.T) { + for _, driver := range drivers { + cache := store(driver) + + cache.Put("key", 100, 1) + + got, err := cache.GetInt("key") + + if got != int64(100) || err != nil { + t.Error("Expected 100, got ", got) + } + + cache.Forget("key") + } +} + +func TestPutGetFloat(t *testing.T) { + for _, driver := range drivers { + cache := store(driver) + + var expected float64 + + expected = 9.99 + + cache.Put("key", expected, 1) + + got, err := cache.GetFloat("key") + + if got != expected || err != nil { + t.Error("Expected 9.99, got ", got) + } + + cache.Forget("key") + } +} + +func TestForever(t *testing.T) { + for _, driver := range drivers { + cache := store(driver) + + expected := "value" + + cache.Forever("key", expected) + + got, err := cache.Get("key") + + if got != expected || err != nil { + t.Error("Expected "+expected+", got ", got) + } + + cache.Forget("key") + } +} + +func TestPutGetMany(t *testing.T) { + for _, driver := range drivers { + cache := store(driver) + + keys := make(map[string]interface{}) + + keys["key_1"] = "value" + keys["key_2"] = int64(100) + keys["key_3"] = float64(9.99) + + cache.PutMany(keys, 10) + + resultKeys := make([]string, 3) + + resultKeys[0] = "key_1" + resultKeys[1] = "key_2" + resultKeys[2] = "key_3" + + results, err := cache.Many(resultKeys) + + if err != nil { + panic(err) + } + + for i, result := range results { + if result != keys[i] { + t.Error("Expected got", results["key_1"]) + } + } + + cache.Flush() + } +} + +func TestPutGetStruct(t *testing.T) { + for _, driver := range drivers { + cache := store(driver) + + var example Example + + example.Name = "Alejandro" + example.Description = "Whatever" + + cache.Put("key", example, 10) + + var newExample Example + + cache.GetStruct("key", &newExample) + + if newExample != example { + t.Error("The structs are not the same", newExample) + } + + cache.Forget("key") + } +} + +func TestIncrement(t *testing.T) { + for _, driver := range drivers { + cache := store(driver) + + cache.Increment("increment_key", 1) + cache.Increment("increment_key", 1) + got, err := cache.GetInt("increment_key") + + cache.Forget("increment_key") + + var expected int64 = 2 + + if got != expected || err != nil { + t.Error("Expected 2, got ", got) + } + } +} + +func TestDecrement(t *testing.T) { + for _, driver := range drivers { + cache := store(driver) + + cache.Increment("decrement_key", 2) + cache.Decrement("decrement_key", 1) + + var expected int64 = 1 + + got, err := cache.GetInt("decrement_key") + + if got != expected || err != nil { + t.Error("Expected "+string(expected)+", got ", got) + } + + cache.Forget("decrement_key") + } +} + +func TesIncrementDecrement(t *testing.T) { + for _, driver := range drivers { + cache := store(driver) + + got, err := cache.Increment("key", 2) + + if got != int64(2) { + t.Error("Expected bar 2", got) + } + + got, err = cache.Increment("key", 8) + + if got != int64(10) { + t.Error("Expected bar 10", got) + } + + got, err = cache.Decrement("key", 10) + + if got != int64(0) { + t.Error("Expected bar 0", got) + } + + got, err = cache.Decrement("key1", 0) + + if got != int64(0) { + t.Error("Expected bar 0", got) + } + + got, err = cache.Increment("key1", 10) + + if got != int64(10) { + t.Error("Expected bar 10", got) + } + + got, err = cache.Decrement("key1", 10) + + if got != int64(0) { + t.Error("Expected bar 0", got) + } + + if err != nil { + panic(err) + } + + cache.Flush() + } +} + +func store(store string) StoreInterface { + switch strings.ToLower(store) { + case REDIS_DRIVER: + cache, err := New(store, redisStore()) + + if err != nil { + panic(err) + } + + return cache + case MEMCACHE_DRIVER: + cache, err := New(store, memcacheStore()) + + if err != nil { + panic(err) + } + + return cache + case MAP_DRIVER: + cache, err := New(store, mapStore()) + + if err != nil { + panic(err) + } + + return cache + } + + panic("No valid driver provided.") +} diff --git a/cache/tag_set.go b/cache/tag_set.go new file mode 100644 index 0000000..ebf082d --- /dev/null +++ b/cache/tag_set.go @@ -0,0 +1,80 @@ +package cache + +import ( + "github.com/segmentio/ksuid" + "strings" +) + +// TagSet is the representation of a tag set for the cahing stores +type TagSet struct { + Store StoreInterface + Names []string +} + +// GetNamespace gets the current TagSet namespace +func (ts *TagSet) GetNamespace() (string, error) { + tagsIds, err := ts.tagIds() + + if err != nil { + return "", err + } + + return strings.Join(tagsIds, "|"), err +} + +// Reset resets the tag set +func (ts *TagSet) Reset() error { + for i, name := range ts.Names { + id, err := ts.resetTag(name) + + if err != nil { + return err + } + + ts.Names[i] = id + } + + return nil +} + +func (ts *TagSet) tagId(name string) (string, error) { + value, err := ts.Store.Get(ts.tagKey(name)) + + if err != nil { + return value.(string), err + } + + if value == "" { + return ts.resetTag(name) + } + + return value.(string), nil +} + +func (ts *TagSet) tagKey(name string) string { + return "tag:" + name + ":key" +} + +func (ts *TagSet) tagIds() ([]string, error) { + tagIds := make([]string, len(ts.Names)) + + for i, name := range ts.Names { + val, err := ts.tagId(name) + + if err != nil { + return tagIds, err + } + + tagIds[i] = val + } + + return tagIds, nil +} + +func (ts *TagSet) resetTag(name string) (string, error) { + id := ksuid.New().String() + + err := ts.Store.Forever(ts.tagKey(name), id) + + return id, err +} diff --git a/cache/tagged_cache.go b/cache/tagged_cache.go new file mode 100644 index 0000000..607d208 --- /dev/null +++ b/cache/tagged_cache.go @@ -0,0 +1,173 @@ +package cache + +import ( + "crypto/sha1" + "encoding/hex" +) + +// TaggedCache is the representation of a tagged caching store +type TaggedCache struct { + Store StoreInterface + Tags TagSet +} + +// Get gets a value from the store +func (tc *TaggedCache) Get(key string) (interface{}, error) { + tagKey, err := tc.taggedItemKey(key) + + if err != nil { + return tagKey, err + } + + return tc.Store.Get(tagKey) +} + +// Put puts a value in the given store for a predetermined amount of time in mins. +func (tc *TaggedCache) Put(key string, value interface{}, minutes int) error { + tagKey, err := tc.taggedItemKey(key) + + if err != nil { + return err + } + + return tc.Store.Put(tagKey, value, minutes) +} + +// Increment increments an integer counter by a given value +func (tc *TaggedCache) Increment(key string, value int64) (int64, error) { + tagKey, err := tc.taggedItemKey(key) + + if err != nil { + return 0, err + } + + return tc.Store.Increment(tagKey, value) +} + +// Decrement decrements an integer counter by a given value +func (tc *TaggedCache) Decrement(key string, value int64) (int64, error) { + tagKey, err := tc.taggedItemKey(key) + + if err != nil { + return 0, err + } + + return tc.Store.Decrement(tagKey, value) +} + +// Forget forgets/evicts a given key-value pair from the store +func (tc *TaggedCache) Forget(key string) (bool, error) { + tagKey, err := tc.taggedItemKey(key) + + if err != nil { + return false, err + } + + return tc.Store.Forget(tagKey) +} + +// Forever puts a value in the given store until it is forgotten/evicted +func (tc *TaggedCache) Forever(key string, value interface{}) error { + tagKey, err := tc.taggedItemKey(key) + + if err != nil { + return err + } + + return tc.Store.Forever(tagKey, value) +} + +// Flush flushes the store +func (tc *TaggedCache) Flush() (bool, error) { + return tc.Store.Flush() +} + +// Many gets many values from the store +func (tc *TaggedCache) Many(keys []string) (map[string]interface{}, error) { + taggedKeys := make([]string, len(keys)) + values := make(map[string]interface{}) + + for i, key := range keys { + tagKey, err := tc.taggedItemKey(key) + + if err != nil { + return values, err + } + + taggedKeys[i] = tagKey + } + + results, err := tc.Store.Many(taggedKeys) + + if err != nil { + return results, err + } + + for i, result := range results { + values[GetTaggedManyKey(tc.GetPrefix(), i)] = result + } + + return values, nil +} + +// PutMany puts many values in the given store until they are forgotten/evicted +func (tc *TaggedCache) PutMany(values map[string]interface{}, minutes int) error { + taggedMap := make(map[string]interface{}) + + for key, value := range values { + tagKey, err := tc.taggedItemKey(key) + + if err != nil { + return err + } + + taggedMap[tagKey] = value + } + + return tc.Store.PutMany(taggedMap, minutes) +} + +// GetPrefix gets the cache key prefix +func (tc *TaggedCache) GetPrefix() string { + return tc.Store.GetPrefix() +} + +// GetInt gets an int value from the store +func (tc *TaggedCache) GetInt(key string) (int64, error) { + return tc.Store.GetInt(key) +} + +// GetFloat gets a float value from the store +func (tc *TaggedCache) GetFloat(key string) (float64, error) { + return tc.Store.GetFloat(key) +} + +// GetStruct gets the struct representation of a value from the store +func (tc *TaggedCache) GetStruct(key string, entity interface{}) (interface{}, error) { + tagKey, err := tc.taggedItemKey(key) + + if err != nil { + return tagKey, err + } + + return tc.Store.GetStruct(tagKey, entity) +} + +// TagFlush flushes the tags of the TaggedCache +func (tc *TaggedCache) TagFlush() error { + return tc.Tags.Reset() +} + +func (tc *TaggedCache) taggedItemKey(key string) (string, error) { + h := sha1.New() + + namespace, err := tc.Tags.GetNamespace() + + if err != nil { + return namespace, err + } + + h.Write(([]byte(namespace))) + + return tc.GetPrefix() + hex.EncodeToString(h.Sum(nil)) + ":" + key, nil +} diff --git a/cache/tagged_cache_test.go b/cache/tagged_cache_test.go new file mode 100644 index 0000000..999772e --- /dev/null +++ b/cache/tagged_cache_test.go @@ -0,0 +1,193 @@ +package cache + +import "testing" + +func TestPutGetWithTags(t *testing.T) { + for _, driver := range drivers { + cache := store(driver) + + expected := "value" + + tags := tag() + + cache.Tags(tags).Put("key", "value", 10) + + got, err := cache.Tags(tags).Get("key") + + if got != expected || err != nil { + t.Error("Expected value, got ", got) + } + + cache.Tags(tags).Forget("key") + } +} + +func TestPutGetIntWithTags(t *testing.T) { + for _, driver := range drivers { + cache := store(driver) + + tags := tag() + + cache.Tags(tags).Put("key", 100, 1) + + got, err := cache.Tags(tags).Get("key") + + if got != int64(100) || err != nil { + t.Error("Expected 100, got ", got) + } + + cache.Tags(tags).Forget("key") + } +} + +func TestPutGetFloatWithTags(t *testing.T) { + for _, driver := range drivers { + cache := store(driver) + + var expected float64 + + expected = 9.99 + + tags := tag() + + cache.Tags(tags).Put("key", expected, 1) + + got, err := cache.Tags(tags).Get("key") + + if got != expected || err != nil { + t.Error("Expected 9.99, got ", got) + } + + cache.Tags(tags).Forget("key") + } +} + +func TestIncrementWithTags(t *testing.T) { + for _, driver := range drivers { + cache := store(driver) + + tags := tag() + + cache.Tags(tags).Increment("increment_key", 1) + cache.Tags(tags).Increment("increment_key", 1) + got, err := cache.Tags(tags).Get("increment_key") + + var expected int64 = 2 + + if got != expected || err != nil { + t.Error("Expected 2, got ", got) + } + + cache.Tags(tags).Forget("increment_key") + } +} + +func TestDecrementWithTags(t *testing.T) { + for _, driver := range drivers { + cache := store(driver) + + tags := tag() + + cache.Tags(tags).Increment("decrement_key", 2) + cache.Tags(tags).Decrement("decrement_key", 1) + + var expected int64 = 1 + + got, err := cache.Tags(tags).Get("decrement_key") + + if got != expected || err != nil { + t.Error("Expected "+string(expected)+", got ", got) + } + + cache.Tags(tags).Forget("decrement_key") + } +} + +func TestForeverWithTags(t *testing.T) { + for _, driver := range drivers { + cache := store(driver) + + expected := "value" + + tags := tag() + + cache.Tags(tags).Forever("key", expected) + + got, err := cache.Tags(tags).Get("key") + + if got != expected || err != nil { + t.Error("Expected "+expected+", got ", got) + } + + cache.Tags(tags).Forget("key") + } +} + +func TestPutGetManyWithTags(t *testing.T) { + for _, driver := range drivers { + cache := store(driver) + + tags := tag() + + keys := make(map[string]interface{}) + + keys["key_1"] = "value" + keys["key_2"] = int64(100) + keys["key_3"] = float64(9.99) + + cache.Tags(tags).PutMany(keys, 10) + + resultKeys := make([]string, 3) + + resultKeys[0] = "key_1" + resultKeys[1] = "key_2" + resultKeys[2] = "key_3" + + results, err := cache.Tags(tags).Many(resultKeys) + + if err != nil { + panic(err) + } + + for i := range results { + if results[i] != keys[i] { + t.Error(i, results[i]) + } + } + + cache.Tags(tags).Flush() + } +} + +func TestPutGetStructWithTags(t *testing.T) { + for _, driver := range drivers { + cache := store(driver) + + tags := make([]string, 3) + + tags[0] = "tag1" + tags[1] = "tag2" + tags[2] = "tag3" + + var example Example + + example.Name = "Alejandro" + example.Description = "Whatever" + + cache.Tags(tags...).Put("key", example, 10) + + var newExample Example + + cache.Tags(tags...).GetStruct("key", &newExample) + + if newExample != example { + t.Error("The structs are not the same", newExample) + } + + cache.Forget("key") + } +} + +func tag() string { + return "tag" +} From a82bd3e5bf9aea8eda387346471d475a94856da5 Mon Sep 17 00:00:00 2001 From: Alejandro Date: Fri, 16 Mar 2018 04:25:43 +0000 Subject: [PATCH 2/7] fixing travis. --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 217b0c3..6944fa3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,9 @@ go: - 1.9.x - tip services: - -postgresql + - postgresql + - redis-server + - memcached before_script: - psql -c 'create database utron;' -U postgres before_install: From 008ab825beadfd8ad061d54c54402ee8c96d533c Mon Sep 17 00:00:00 2001 From: Alejandro Date: Fri, 16 Mar 2018 04:58:04 +0000 Subject: [PATCH 3/7] changing uuid to xid to support older go versions. --- cache/tag_set.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cache/tag_set.go b/cache/tag_set.go index ebf082d..fe3fa36 100644 --- a/cache/tag_set.go +++ b/cache/tag_set.go @@ -1,7 +1,7 @@ package cache import ( - "github.com/segmentio/ksuid" + "github.com/rs/xid" "strings" ) @@ -72,7 +72,7 @@ func (ts *TagSet) tagIds() ([]string, error) { } func (ts *TagSet) resetTag(name string) (string, error) { - id := ksuid.New().String() + id := xid.New().String() err := ts.Store.Forever(ts.tagKey(name), id) From 0cd51a08a16b4c6a8b9693de4a6a7b03ed221ddc Mon Sep 17 00:00:00 2001 From: Alejandro Date: Fri, 16 Mar 2018 05:09:38 +0000 Subject: [PATCH 4/7] allowing failures for versions older than 1.7 --- .travis.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.travis.yml b/.travis.yml index 6944fa3..cd9cb5e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,5 +15,11 @@ before_script: before_install: - go get -t -v - go get github.com/mattn/goveralls + +matrix: + allow_failures: + - go: 1.5.x + - go: 1.6.x + script: - $HOME/gopath/bin/goveralls -v -service=travis-ci -repotoken=$COVERALLS From 988b3ff7f7ac1c91867b0dc7b6c20fc204bd0403 Mon Sep 17 00:00:00 2001 From: Alejandro Carstens Cattori Date: Fri, 16 Mar 2018 17:41:10 -0700 Subject: [PATCH 5/7] Update .travis.yml --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index cd9cb5e..52872bf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,7 @@ matrix: allow_failures: - go: 1.5.x - go: 1.6.x + - tip script: - $HOME/gopath/bin/goveralls -v -service=travis-ci -repotoken=$COVERALLS From 9520b29e57823a55cb10a51d8957ccd8eebe58f1 Mon Sep 17 00:00:00 2001 From: Alejandro Carstens Cattori Date: Fri, 16 Mar 2018 17:42:37 -0700 Subject: [PATCH 6/7] Update .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 52872bf..9b962a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ matrix: allow_failures: - go: 1.5.x - go: 1.6.x - - tip + - go: tip script: - $HOME/gopath/bin/goveralls -v -service=travis-ci -repotoken=$COVERALLS From cd89856383b3c2181382045252eb46a084a1d641 Mon Sep 17 00:00:00 2001 From: Alejandro Date: Sat, 17 Mar 2018 05:00:15 +0000 Subject: [PATCH 7/7] adding TagSet test. --- cache/contracts.go | 2 ++ cache/tagged_cache.go | 5 +++++ cache/tagged_cache_test.go | 24 ++++++++++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/cache/contracts.go b/cache/contracts.go index ed5a0b2..4fd05af 100644 --- a/cache/contracts.go +++ b/cache/contracts.go @@ -53,4 +53,6 @@ type TaggedStoreInterface interface { CacheInterface TagFlush() error + + GetTags() TagSet } diff --git a/cache/tagged_cache.go b/cache/tagged_cache.go index 607d208..844860e 100644 --- a/cache/tagged_cache.go +++ b/cache/tagged_cache.go @@ -158,6 +158,11 @@ func (tc *TaggedCache) TagFlush() error { return tc.Tags.Reset() } +// GetTags returns the TaggedCache Tags +func (tc *TaggedCache) GetTags() TagSet { + return tc.Tags +} + func (tc *TaggedCache) taggedItemKey(key string) (string, error) { h := sha1.New() diff --git a/cache/tagged_cache_test.go b/cache/tagged_cache_test.go index 999772e..9cd9b10 100644 --- a/cache/tagged_cache_test.go +++ b/cache/tagged_cache_test.go @@ -188,6 +188,30 @@ func TestPutGetStructWithTags(t *testing.T) { } } +func TestTagSet(t *testing.T) { + for _, driver := range drivers { + cache := store(driver) + + tagSet := cache.Tags("Alejandro").GetTags() + + namespace, err := tagSet.GetNamespace() + + if err != nil { + panic(err) + } + + if len([]rune(namespace)) != 20 { + t.Error("The namespace is not 20 chars long.", namespace) + } + + got := tagSet.Reset() + + if got != nil { + t.Error("Reset did not return nil.", got) + } + } +} + func tag() string { return "tag" }